Blog

๐Ÿค” Mengapa Gato GraphQL yang baru butuh 1,5 tahun untuk dirilis?

Leonardo Losoviz
Oleh Leonardo Losoviz ยท

Versi 0.9 dari Gato GraphQL baru saja dirilis. Dibutuhkan hampir 1,5 tahun pengembangan, dan lebih dari 16000 commit, untuk siap. Itu memang waktu yang sangat lama!

Setelah berbagi pengumuman di Hacker News, saya mendapatkan pertanyaan berikut:

[...] Saya ingin tahu apa yang membutuhkan 16k commit. Proyek-proyek yang saya ikuti dengan lebih dari sepuluh ribu commit melibatkan puluhan atau ratusan orang yang bekerja penuh waktu. [...] Apakah ada kompleksitas tertentu yang perlu diatasi yang tidak disebutkan dalam tulisan tersebut?

Jumlah commit bukanlah metrik yang sangat andal, karena saya mungkin hanya melakukan perubahan sangat sederhana dan mendorongnya sebagai satu commit. Banyak dari 16k commit tersebut adalah commit "typo", atau sekadar memperbaiki deskripsi di beberapa README.

Meskipun demikian, jumlah commit memang memberi gambaran tentang upaya nyata yang terlibat. Ada juga banyak commit yang padat dengan modifikasi, termasuk puluhan, bahkan ratusan perubahan sekaligus. Perubahan antara versi 0.8 dan 0.9 memang sangat besar, dan itu membutuhkan upaya serta waktu untuk diselesaikan.

Dalam tulisan blog ini, saya akan menjelaskan apa saja perubahan tersebut, untuk menjelaskan mengapa butuh waktu begitu lama. Dan dalam melakukannya, saya juga akan memberikan pratinjau beberapa fitur canggih yang ditambahkan ke basis kode, dan yang akan melihat cahaya hari dengan versi 1.0 yang akan datang.

Latar Belakang Server GraphQL

Pertama, saya akan berbagi sedikit sejarah engine ini, beserta detail teknis cara kerjanya.

(Ini sebagian besar relevan untuk developer; jika Anda tidak tertarik dengan hal teknis, silakan lewati ke bagian berikutnya.)

Gato GraphQL dibangun di atas PoP, sebuah engine yang merender komponen dalam PHP (mirip dengan React atau Vue di JavaScript). Ketergantungannya pada engine ini bersifat mutlak, itulah mengapa plugin ini di-host di bawah monorepo GatoGraphQL/GatoGraphQL di GitHub.

Di balik layar, ketergantungan ini terlihat seperti ini:

Gato GraphQL menyelesaikan sebuah GraphQL query dengan terlebih dahulu mengubahnya menjadi model komponen yang setara, yang kemudian diselesaikan oleh PoP dengan mengambil semua data yang diperlukan, dan kemudian data ini diberikan bentuk sesuai GraphQL query tersebut.

Ketika saya mulai mengerjakan PoP sekitar tahun 2013/2014, belum ada GraphQL, dan metodologi untuk menyelesaikan model komponen menjadi data dirancang dan diimplementasikan dari awal. Tidak adanya model yang bisa diikuti (seperti GraphQL untuk konsep, dan proyek referensi graphql-js untuk implementasi) telah menjadi hambatan sekaligus berkah, seperti yang akan saya jelaskan nanti.

PoP awalnya dirancang untuk merender seluruh situs web sebagai HTML di sisi server, sambil mengekspos data mentah dalam format JSON saat menambahkan ?output=json ke URL halaman, dan lebih lanjut memilih data apa yang akan diambil (settings, data objek DB) dengan parameter URL tambahan.

Silakan klik tautan berikut (semuanya mengarah ke halaman web yang sama, hanya dengan parameter URL yang berbeda) dan perhatikan perbedaannya:

Saat mengklik tautan terakhir, sebuah kesadaran muncul: Ini pada dasarnya adalah GraphQL! Satu-satunya perbedaan besar adalah bahwa data dalam respons bersifat implisit, karena sudah didefinisikan oleh komponen (dalam PHP) yang disertakan di halaman. GraphQL, sebaliknya, memungkinkan kita memutuskan data apa yang akan diambil melalui sebuah query.

Jadi ketika saya belajar tentang GraphQL sekitar tahun 2019, sudah jelas bagi saya untuk membuat PoP juga memenuhi kebutuhan server GraphQL. Yang perlu dilakukan hanyalah menerima GraphQL query sebagai input, dan membuat model komponen secara on-the-fly berdasarkan query tersebut.

Dan itulah yang saya lakukan. Dan itu berjalan dengan baik. Tetapi lambat, karena PoP memahami format inputnya sendiri, sehingga GraphQL query harus diadaptasi ke format PoP:

  1. Parse GraphQL query; kemudian
  2. Ubah query ke format PoP; kemudian
  3. Parse format PoP

Parsing GraphQL query kemudian dilakukan dua kali (sekali untuk GraphQL, sekali untuk PoP), dan format PoP tidak diselesaikan melalui AST, melainkan hanya dengan mengurai string query berulang kali. (Tidak menggunakan AST adalah cara pengkodean yang buruk, tetapi saya tidak memiliki spec yang bisa diikuti, dan pengembangannya terjadi secara organik, di mana substr(...) sederhana bisa menyelamatkan hari, setiap hari.)

Inilah mengapa saya mengatakan bahwa tidak memiliki spec GraphQL adalah sebuah hambatan, karena solusi saya lambat (dan inilah situasinya pada versi 0.8). Jadi saya memutuskan untuk memperbaikinya.

Mengubah Engine Menjadi GraphQL-first

Solusi yang saya putuskan adalah membuat PoP berbicara bahasa GraphQL secara native. Kemudian, meneruskan GraphQL query ke PoP sebagai input sudah akan dikonversi ke model komponen, tanpa perlu adapter tambahan, atau melakukan hal yang sama dua kali.

Ini berarti proyek PoP harus diubah tujuannya, dari menjadi library PHP yang merender komponen untuk situs web di sisi server yang diadaptasi untuk menyelesaikan GraphQL query, menjadi benar-benar menjadi server GraphQL.

Basis kode kemudian mengalami transformasi besar-besaran, memperkenalkan AST GraphQL sebagai fondasi untuk mengkomunikasikan state di seluruh layanan PHP dalam engine. Objek-objek AST GraphQL kini menjadi input ke PoP (bukan string query).

Server GraphQL lain dalam PHP mengandalkan graphql-php, tetapi plugin Gato GraphQL tidak. Ini adalah kabar buruk terkait upaya pemeliharaan (karena saya tidak bisa menggunakan kembali apa yang telah dikodekan orang lain), tetapi kabar baik terkait kemandirian: saya bisa memutuskan untuk menambahkan fitur kustom ke plugin saya dengan kecepatan sendiri, dan menurut kriteria saya sendiri (itulah mengapa plugin ini sudah menyediakan "oneof" input object).

Dan seperti yang akan ditunjukkan di bagian di bawah ini, ini adalah keunggulan yang luar biasa.

Menggabungkan Fitur Orisinal ke GraphQL

GraphQL biasanya dikaitkan dengan pengambilan data. Tentu saja, Anda dapat mengambil data apa pun (posts, users, comments, dll.) dari 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
    }
  }
}

Tetapi ini adalah hal yang mudah. GraphQL juga dapat digunakan untuk banyak kasus penggunaan lain, termasuk manipulasi dan transformasi data, bahkan menempatkan GraphQL dalam pipeline untuk menjadi perantara antar layanan.

Beberapa contoh di mana GraphQL berguna:

  • Mengekstrak informasi dari satu atau lebih sumber (seperti pengguna dari situs WordPress dan data kontak newsletter dari Mailchimp), menggabungkan data, dan menganalisisnya semua bersama sebagai satu dataset
  • Menjalankan operasi untuk mengadaptasi konten di situs:
    • Sebagai one-off, seperti saat memigrasikan situs ke domain lain dan mengganti "www.myoldsite.com" menjadi "mynewsite.com" di mana-mana dalam konten dan metadata
    • Secara berkelanjutan, untuk mengganti "http://" menjadi "https://" setiap kali seorang penulis menerbitkan tulisan blog baru
  • Terhubung ke Google Translate API untuk menerjemahkan semua tulisan blog ke bahasa yang berbeda
  • Mengirim tweet secara otomatis setelah tulisan blog diterbitkan

PoP telah dirancang untuk mendukung kasus penggunaan lain ini, melalui fitur-fitur yang tidak (secara alami) didukung oleh GraphQL, seperti:

  • Mendukung field "fungsionalitas" (selain field "data"), yang ditambahkan ke semua tipe dalam schema
  • Meneruskan hasil sebuah field sebagai input ke field lain, dalam query yang sama
  • Menyusun direktif, agar sebuah direktif dapat mengubah perilaku direktif lain
  • Memutuskan untuk menerapkan direktif atau tidak secara dinamis, berdasarkan nilai field tersebut

Dan saya tentu saja tidak ingin menghapus fitur-fitur ini dari server GraphQL: saya sudah mengkodekannya, dan fitur-fitur tersebut jelas bernilai.

Jadi alasan kedua mengapa v0.9 membutuhkan waktu begitu lama adalah bahwa saya juga harus menemukan cara untuk menggabungkan kemampuan-kemampuan baru ini ke dalam GraphQL, dengan cara yang tidak merusak spec GraphQL (misalnya, memperkenalkan elemen baru ke sintaks GraphQL bukanlah pilihan yang bisa dilakukan).

Contoh Manipulasi Data dalam GraphQL

Kemampuan-kemampuan baru yang diperkenalkan ke GraphQL dalam plugin ini akan semakin terlihat di masa mendatang, ketika versi 1.0 dirilis. Tetapi Anda sudah bisa merasakan sebagian dari fitur tersebut sekarang.

GraphQL query berikut mengambil daftar entri pengguna dari REST API eksternal (yang dapat di-@remove dari respons); memasukkan data ini ke field lain, di dalam query yang sama; mengekstrak properti email dari setiap entri; dan akhirnya mengubah email menjadi huruf kapital, tetapi hanya jika bahasa pada entri yang sama adalah bahasa Inggris atau Jerman:

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

Ini adalah responnya (perhatikan bagaimana hanya email tertentu yang diubah menjadi huruf kapital):

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

Coba sendiri! Tekan tombol "Run" untuk menjalankan 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
}

Saya telah menyebutkan bahwa tidak dibimbing oleh GraphQL adalah sebuah hambatan, tetapi (secara retrospektif) juga sebuah berkah. Ini karena saya tidak memiliki batasan-batasan spec GraphQL, sehingga saya bisa bermimpi tentang kemampuan-kemampuan baru ini.

Dan sekarang setelah fitur-fitur ini dimigrasikan ke Gato GraphQL, ia bisa menjadi sekutu yang sangat berguna untuk segala hal terkait pengambilan, manipulasi, dan transformasi konten untuk situs WordPress Anda. (Meskipun hanya akan dapat diakses dengan v1.0 yang akan datang).

Membutuhkan waktu yang cukup lama, tetapi upaya tersebut jelas sepadan.

Coba Sekarang!

Apakah Anda yakin bahwa penantian panjang ini sepadan? Saya harap begitu!

Silakan, unduh plugin ini, dan lihat sendiri:

Tertarik mendapatkan berita mengenai pengembangannya, dokumentasi baru, dan rilis mendatang, termasuk v1.0? Silakan berlangganan newsletter.

Ingin menjelajahi kode open source di GitHub? Lihat GatoGraphQL/GatoGraphQL (dan silakan beri bintang... Kami suka bintang! โญ๏ธโญ๏ธโญ๏ธ)

Omong-omong, transformasi konten apa yang perlu Anda lakukan di WordPress (yang mungkin sudah Anda tangani dengan plugin komersial khusus)? Silakan kirim pesan kepada saya mengenai kasus penggunaan Anda.

Jika Anda menyukai apa yang Anda lihat, tolong bagikan kepada teman dan kolega Anda, bantu sebarkan kasih sayang โค๏ธ.


Berlangganan newsletter kami

Tetap update dengan semua pembaruan Gato GraphQL.