๐ถ๐ป Meremajakan WordPress melalui GraphQL
WordPress adalah CMS warisan: diciptakan lebih dari 17 tahun lalu, ia dipenuhi kode PHP yang, jika diberi kesempatan baru, akan ditulis dengan cara berbeda.
GraphQL adalah antarmuka modern untuk mengakses data. Perhatikan kata "antarmuka": ia tidak peduli bagaimana sistem data yang mendasarinya diimplementasikan, tetapi hanya bagaimana cara mengekspos data tersebut.
Apa yang terjadi ketika kita menyatukan keduanya? Bagaimana seharusnya kita merancang antarmuka GraphQL untuk mengakses data dari WordPress?
Ada beberapa strategi jelas yang dapat kita terapkan:
-
Menghormati tradisi, dan menyediakan pemetaan yang mempertahankan model data WordPress apa adanya, termasuk utang teknis yang menumpuk selama bertahun-tahun
-
Memperbaiki utang teknis, menyediakan antarmuka yang mengekspos data secara abstrak, tidak harus terikat pada WordPress
Kedua pendekatan memiliki keuntungan dan kekurangan, dan tidak ada yang benar atau salah. Ini hanyalah sebuah pendapat, memprioritaskan satu perilaku di atas yang lain.
Untuk plugin Gato GraphQL saya telah memilih pendekatan kedua, berusaha menciptakan skema GraphQL yang, meskipun berbasis WordPress dan bekerja untuk WordPress, tidak terikat pada WordPress (misalnya, dengan menghapus nama dan hubungan yang tidak konsisten).
Hasilnya adalah GraphQL meremajakan WordPress: meskipun kita masih memiliki WordPress sebagai CMS yang mendasarinya, dengan kode PHP warisannya, lapisan datanya dapat dibuat ulang, berdasarkan akal sehat, bukan tradisi. Lapisan data kembali dari menjadi remaja, menjadi balita lagi.

Hasilnya adalah skema GraphQL yang merepresentasikan model data WordPress, dan juga mendukung nested mutations.
Mari kita lihat bagaimana hal itu dilaksanakan.
Model data WordPress
WordPress memiliki entitas-entitas berikut:
- posts
- halaman
- custom posts
- elemen media
- pengguna
- peran pengguna
- tags
- kategori
- komentar
- blok
- properti meta
- lainnya (opsi, plugin, tema, dll)
Entitas-entitas ini dapat memiliki hierarki. Misalnya, post, halaman, dan elemen media semuanya adalah custom post types, dan tags serta kategori keduanya adalah taksonomi.
Berikut adalah diagram database WordPress, yang menunjukkan bagaimana data untuk semua entitas disimpan:

Apakah pemetaan merupakan replika persis dari diagram DB?
Saat memetakan database WordPress ke dalam skema GraphQL, apakah diagram yang sama di atas dihormati 1 berbanding 1?
Tidak. Meskipun diagram database adalah implementasi nyata, GraphQL adalah antarmuka untuk mengakses data dari klien. Keduanya saling berkaitan, tetapi bisa berbeda. GraphQL tidak peduli tentang database: ia tidak berpikir dalam perintah SQL, atau tahu bahwa ada tabel database bernama wp_posts dan wp_users.
Jadi kita tidak perlu terlalu khawatir tentang diagram database saat membuat skema GraphQL untuk WordPress. Itu berarti kita dapat menghasilkan skema GraphQL yang memperbaiki sebagian utang teknis dari model data WordPress.
Memetakan model data WordPress sebagai skema GraphQL
Mari kita lakukan pemetaan. Pertama, kita memetakan entitas asli sebagai tipe, sebisa mungkin. Dari daftar entitas dalam model data WordPress, kita menghasilkan tipe-tipe berikut untuk skema GraphQL:
PostPageMediaUserUserRolePostTagPostCategoryComment
Kemudian, kita menambahkan semua field yang diharapkan ke setiap tipe. Untuk merepresentasikan skema, kita dapat menggunakan SDL, atau Schema Definition Language. (Ini digunakan hanya untuk tujuan dokumentasi; plugin itu sendiri tidak menggunakan SDL untuk mengkodekan skema: semuanya kode PHP).
Berikut adalah field-field (di antara banyak lainnya) untuk sebuah Post:
type Post {
id: ID!
title: String
content: String
excerpt: String
publishedAt: Date!
}Berikut adalah field-field (di antara banyak lainnya) untuk sebuah User:
type User {
id: ID!
name: String
email: String!
}Kita juga membuat koneksi yang sesuai, yaitu field yang mengembalikan entitas lain (bukan skalar, seperti angka atau string). Misalnya, kita merepresentasikan sebuah post yang memiliki penulis, dan seorang pengguna yang memiliki post:
type Post {
author: User!
}
type User {
posts: [Post]
}Field dan koneksi juga dapat menerima argumen. Misalnya, kita mengaktifkan Post.date agar dapat diformat, dan User.posts untuk mencari entri dan membatasi jumlahnya:
type Post {
date(format: String): Date!
}
type User {
posts(limit: Int, search: String): [Post]
}Kita terus melakukan ini untuk semua entitas dalam model data WordPress. Setelah selesai, kita akan tiba pada skema GraphQL untuk WordPress, seperti yang terlihat menggunakan klien Voyager (tersedia sebagai "Interactive Schema" di menu plugin):

Skema ini memiliki kemiripan dengan diagram database WordPress, tetapi juga banyak perbedaan. Mari kita analisis.
Operasi tanpa entitas dipetakan sebagai field Root
Diagram database WordPress merepresentasikan bagaimana data disimpan, sehingga tidak ada "titik awal". GraphQL, bagaimanapun, adalah antarmuka untuk mengambil data, sehingga harus ada tahap awal dari mana query dieksekusi.
Tahap awal ini adalah tipe Root, atau, lebih tepatnya, tipe QueryRoot dan MutationRoot (untuk menangani queries dan mutations, masing-masing).
Dalam dua tipe ini, kita memetakan semua operasi yang tidak bergantung pada entitas, seperti saat mengeksekusi get_posts(), get_users(), atau wp_signon():
type QueryRoot {
posts: [Post]!
users: [User]!
}
type MutationRoot {
logUserIn(username: String, password: String): User
}Field tidak perlu memiliki nama atau tanda tangan yang sama dengan operasi yang mereka representasikan. Misalnya, memanggil field logUserIn dapat dianggap lebih tepat daripada signOn.
Semua mutations berada di bawah MutationRoot
Ada operasi yang memang bergantung pada entitas, seperti wp_update_post(), yang diterapkan pada sebuah post. Mutation yang sesuai dalam skema GraphQL harus ditambahkan ke tipe MutationRoot, karena itulah cara GraphQL bekerja.
Kemudian, operasi ini dipetakan seperti ini:
type MutationRoot {
updatePost(input: {
postID: ID!,
newTitle: String,
newContent: String
}): Post
}Plugin ini juga mendukung nested mutations, yang ditawarkan sebagai fitur opt-in (karena ini bukan perilaku GraphQL standar). Kemudian, mutations juga dapat ditambahkan di bawah tipe apa pun, tidak hanya MutationRoot. Dalam kasus ini, kita mendapatkan:
type Post {
update(input: {
newTitle: String,
newContent: String
}): Post!
}Menangani custom posts
Tidak ada pewarisan tipe dalam GraphQL. Karenanya, kita tidak bisa memiliki tipe CustomPost, dan mendeklarasikan bahwa Post dan Page memperluasnya.
GraphQL menawarkan dua sumber daya untuk mengimbangi kekurangan ini: interfaces dan union types.
Untuk yang pertama, kita membuat interface CustomPost untuk skema, mendeklarasikan semua field yang diharapkan dari sebuah custom post, dan kita mendefinisikan tipe Post dan Page untuk mengimplementasikan interface tersebut:
interface CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}
type Post implements CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}
type Page implements CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}Untuk yang kedua, kita membuat tipe CustomPostUnion untuk skema yang mengembalikan semua custom post types:
union CustomPostUnion = Post | PageDan membuat field mengembalikan tipe ini bila sesuai:
type QueryRoot {
customPost(id: ID): CustomPostUnion
customPosts: [CustomPostUnion]!
}
type User {
customPosts: [CustomPostUnion]
}
type Comment {
customPost: CustomPostUnion!
}Seperti yang dapat diamati, dalam skema GraphQL kita perlu secara eksplisit menyatakan kapan kita berurusan dengan posts, dan kapan dengan custom posts, karena keduanya tidak sama! Menyebut keduanya secara bergantian adalah utang teknis dari WordPress, yang dapat kita perbaiki.
Untuk alasan ini, sebuah custom post selalu disebut CustomPost dan bukan Post, sebuah field yang berurusan dengan custom posts selalu disebut customPosts dan bukan posts, dan argumen field yang menerima ID untuk sebuah custom post disebut customPostID dan bukan postID (meskipun itulah sebutan dalam fungsi WordPress yang dipetakan).
Dengan demikian, ekspektasinya selalu jelas:
- field
User.customPostsdapat mengembalikan daftar custom post apa pun, termasuk posts dan halaman, danUser.postshanya mengembalikan posts - field
Root.setFeaturedImageOnCustomPostdapat menambahkan gambar unggulan ke custom post apa pun, itulah mengapa tidak disebutsetFeaturedImageOnPost
Tidak mengelompokkan tags (dan kategori) dalam satu tipe
Mengapa tipe PostTag (dan sama halnya PostCategory) dinamai demikian, bukan sekadar Tag?
Karena, saat mengeksekusi query ini (di mana sebuah produk adalah CPT), hasil dari field tags untuk posts dan produk akan selalu berbeda, tidak tumpang tindih:
query {
posts {
tags {
id
name
}
}
products {
tags {
id
name
}
}
}Tags yang ditambahkan ke posts tidak akan muncul saat mengambil tags untuk produk, dan sebaliknya (kecuali sebuah produk juga menggunakan taksonomi post_tag, tetapi kemudian juga dapat direpresentasikan dengan tipe PostTag). Ini tidak terlalu menjadi masalah besar di WordPress, karena item-item ini dapat dianggap sebagai baris berbeda dari tabel database yang sama. Tetapi hal ini penting untuk GraphQL, yang bertipe kuat.
Maka, ini adalah keputusan desain yang baik untuk memisahkan entitas-entitas ini, di bawah tipe masing-masing, dan membuat tags untuk posts dikembalikan di bawah tipe PostTag dan, jika sebuah plugin kustom mengimplementasikan CPT produknya sendiri, maka harus menggunakan tipe ProductTag untuk tags-nya.
Memberikan item media identitasnya sendiri
Entitas media di WordPress adalah custom post types, hanya karena hal itu nyaman dari sudut pandang implementasi. Namun, skema GraphQL dapat menghindari utang teknis ini, dan memodelkan elemen media sebagai entitas yang berbeda, bukan sebagai custom posts.
Ini menyiratkan keputusan-keputusan berikut untuk skema GraphQL:
- Saat melakukan query pada field
customPosts, ia tidak akan mengambil elemen media - Tipe
Mediatidak mengimplementasikan interfaceCustomPost, dan tidak akan menjadi bagian dari tipeCustomPostUnion - Tipe
Mediatidak memiliki banyak field yang diharapkan dari sebuah custom post type, sepertiexcerpt,date, danstatus. Sebaliknya, ia hanya memiliki field-field yang diharapkan dari sebuah elemen media:
type Media {
id: ID!
src: String!
width: Int
height: Int
}Mengidentifikasi dan memetakan enum
Dalam beberapa situasi, WordPress menggunakan nilai tetap dari sekumpulan nilai tertentu. Misalnya, status sebuah post hanya bisa berupa "publish", "draft", "pending", atau "trash".
Dalam GraphQL, kita dapat memperlakukan ini sebagai enum (bukan string), dan membuat tipe enumerasi yang sesuai. Mengikuti standar GraphQL, enum harus ditulis dalam huruf kapital, seperti ini:
enum CUSTOM_POST_STATUS {
PUBLISH
DRAFT
PENDING
TRASH
}Namun, query tersebut kemudian tidak dapat langsung digunakan untuk berinteraksi dengan WordPress, karena mengeksekusi get_posts( [ "post_status" => "PUBLISH" ] ) tidak berfungsi.
Jadi, sebagai kompromi, kita mempertahankan nilai enum ini dalam huruf kecil:
enum CUSTOM_POST_STATUS {
publish
draft
pending
trash
}Memetakan tipe tambahan
Blok tidak langsung terlihat dalam diagram database WordPress, karena disimpan di wp_posts (tidak ada tabel wp_blocks), namun demikian mereka adalah entitas yang berbeda.
Karenanya, kita memperkenalkan tipe Block untuk memetakan mereka:
type Post {
blocks: [Block]
}
type Block {
type: String!
attributes: JSONObject
}