Bagaimana plugin memetakan model data WordPress ke skema GraphQL
Inilah cara Gato GraphQL memetakan model data WordPress ke skema GraphQL yang bersesuaian.
Model data WordPress
WordPress memiliki entitas-entitas berikut:
- posts
- pages
- custom posts
- elemen media
- pengguna
- peran pengguna
- tag
- kategori
- komentar
- blok
- properti meta
- lainnya (opsi, plugin, tema, dll.)
Entitas-entitas ini dapat memiliki hierarki. Misalnya, post, page, dan elemen media semuanya merupakan custom post types, dan tag serta kategori keduanya merupakan taksonomi.
Ini adalah diagram basis data WordPress, yang menunjukkan bagaimana data untuk semua entitas disimpan:

Apakah pemetaan merupakan replika persis dari diagram BD?
Saat memetakan basis data WordPress ke dalam skema GraphQL, apakah diagram yang sama di atas dihormati secara 1 banding 1?
Tidak, tidak demikian. Meskipun diagram basis data adalah implementasi nyata, GraphQL adalah antarmuka untuk mengakses data dari klien. Keduanya saling berkaitan, tetapi bisa berbeda. GraphQL tidak peduli dengan basis data: ia tidak berpikir dalam perintah SQL, atau mengetahui adanya tabel basis data bernama wp_posts dan wp_users.
Jadi kita tidak perlu terlalu khawatir tentang diagram basis data saat membuat skema GraphQL untuk WordPress. Bahkan, 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 petakan entitas asli sebagai tipe, sebisa mungkin. Dari daftar entitas dalam model data WordPress, kita menghasilkan tipe-tipe berikut untuk skema GraphQL:
PostPageMediaUserUserRolePostTagPostCategoryComment
Kemudian, kita tambahkan semua field yang diharapkan ke setiap tipe. Untuk merepresentasikan skema, kita dapat menggunakan SDL, atau Schema Definition Language. (Ini hanya digunakan untuk keperluan dokumentasi; plugin itu sendiri tidak menggunakan SDL untuk mengodekan skema: semuanya adalah kode PHP).
Ini adalah field-field (di antara banyak lainnya) untuk sebuah Post:
type Post {
id: ID!
title: String
content: String
excerpt: String
date: Date!
}Ini adalah field-field (di antara banyak lainnya) untuk sebuah User:
type User {
id: ID!
name: String
email: String!
}Kita juga membuat koneksi yang bersesuaian, yaitu field yang mengembalikan entitas lain (alih-alih skalar, seperti angka atau string). Misalnya, kita merepresentasikan sebuah post yang memiliki penulis, dan seorang pengguna yang memiliki posts:
type Post {
author: User!
}
type User {
posts: [Post]
}Field dan koneksi juga dapat menerima argumen. Misalnya, kita mengaktifkan Post.dateStr agar dapat diformat, dan User.posts agar dapat memfilter entri, membatasi jumlahnya, dan mengurutkannya:
type Post {
dateStr(format: String): Date!
}
type User {
posts(
filter: RootPostsFilterInput
pagination: PostPaginationInput
sort: CustomPostSortInput
): [Post!]!
}
input RootPostsFilterInput {
authorIDs: [ID!]
authorSlug: String
categoryIDs: [ID!]
dateQuery: [DateQueryInput!]
excludeAuthorIDs: [ID!]
excludeIDs: [ID!]
hasPassword: Boolean = false
ids: [ID!]
isSticky: Boolean
metaQuery: [CustomPostMetaQueryInput!]
password: String
search: String
status: [FilterCustomPostStatusEnum!]
tagIDs: [ID!]
tagSlugs: [String!]
}
input PostPaginationInput {
limit: Int
offset: Int
}
input CustomPostSortInput {
by: CustomPostOrderByEnum
order: OrderEnum
}
# ...Kita terus melakukan ini untuk semua entitas dalam model data WordPress. Setelah selesai, kita akan tiba pada skema GraphQL untuk WordPress, yang terlihat menggunakan klien Voyager (tersedia sebagai "Interactive Schema" di menu plugin):

Skema ini memiliki kemiripan dengan diagram basis data WordPress, tetapi juga beberapa perbedaan. Mari kita analisis.
Operasi tanpa entitas dipetakan sebagai field Root
Diagram basis data WordPress merepresentasikan bagaimana data disimpan, sehingga tidak ada "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 query dan mutasi, masing-masing).
Dalam dua tipe ini, kita petakan 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 {
loginUser(
usernameOrEmail: String!,
password: String!
): User
}Field-field tidak perlu memiliki nama atau tanda tangan yang sama dengan operasi yang mereka representasikan. Misalnya, memanggil field loginUser dapat dianggap lebih sesuai daripada signOn.
Mengelompokkan elemen skema
Kita dapat menerapkan peningkatan untuk menyederhanakan skema dan membuatnya lebih berguna. Misalnya, sebuah field dapat menerima semua argumennya melalui objek input, yang dapat digunakan kembali di beberapa field dan memudahkan visualisasi skema:
type MutationRoot {
loginUser(input: LoginUserByInput!): User
}
input LoginUserByInput {
usernameOrEmail: String!,
password: String!
}Selain itu, respons dari sebuah mutasi dapat berupa objek "payload", yang selain mengembalikan objek yang terpengaruh juga dapat menyertakan status operasi dan pesan kesalahan:
type MutationRoot {
loginUser(input: LoginUserByInput!): RootLoginUserMutationPayload!
}
type RootLoginUserMutationPayload {
errors: [RootLoginUserMutationErrorPayloadUnion!]
status: OperationStatusEnum!
user: User
userID: ID
}
union RootLoginUserMutationErrorPayloadUnion = GenericErrorPayload
| InvalidUserEmailErrorPayload
| InvalidUsernameErrorPayload
| PasswordIsIncorrectErrorPayload
| UserIsLoggedInErrorPayloadSemua mutasi berada di bawah MutationRoot
Ada operasi yang memang bergantung pada entitas, seperti wp_update_post(), yang diterapkan pada suatu post. Mutasi yang bersesuaian dalam skema GraphQL harus ditambahkan ke tipe MutationRoot, karena itulah cara kerja GraphQL.
Kemudian, operasi ini dipetakan seperti ini:
type MutationRoot {
updatePost(input: RootUpdatePostFilterInput!): PostUpdateMutationPayload!
}
input RootUpdatePostFilterInput {
categoryIDs: [ID!]
content: String
featuredImageID: ID
id: ID!
status: CustomPostStatusEnum
tags: [String!]
title: String
}Plugin ini juga mendukung nested mutations, yang ditawarkan sebagai fitur opt-in (karena ini bukan perilaku standar GraphQL). Kemudian, mutasi juga dapat ditambahkan di bawah tipe mana pun, bukan hanya MutationRoot. Dalam kasus ini, kita mendapatkan:
type Post {
update(input: PostUpdateFilterInput!): PostUpdateMutationPayload!
}
input PostUpdateFilterInput {
categoryIDs: [ID!]
content: String
featuredImageID: ID
status: CustomPostStatusEnum
tags: [String!]
title: String
}Perhatikan perbedaan antara input RootUpdatePostFilterInput dan PostUpdateFilterInput (yaitu, antara mutasi dari root, dan nested mutations): yang pertama memiliki properti wajib id untuk menunjukkan post mana yang akan dimodifikasi, tetapi yang kedua tidak, karena tidak memerlukannya.
Menangani custom posts
Tidak ada pewarisan tipe dalam GraphQL. Oleh karena itu, kita tidak dapat memiliki tipe CustomPost, dan mendeklarasikan bahwa Post dan Page memperluasnya.
GraphQL menawarkan dua sumber daya untuk mengompensasi kekurangan ini: interface 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, Page, dan GenericCustomPost (untuk merepresentasikan semua custom post types yang didefinisikan oleh tema dan plugin yang terpasang) 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!
}
type GenericCustomPost 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 | Page | GenericCustomPostDan membuat field-field mengembalikan tipe ini bila sesuai:
type QueryRoot {
customPost(id: ID): CustomPostUnion
customPosts: [CustomPostUnion]!
}
type User {
customPosts: [CustomPostUnion]
}
type Comment {
customPost: CustomPostUnion!
}Saat mengeksekusi query, kita dapat memilih field berdasarkan tipe aktual, seperti Post, atau berdasarkan interface CustomPost:
{
customPosts {
__typename
...on CustomPost {
id
title
slug
status
}
...on Post {
isSticky
postFormat
}
}
}Sebagaimana 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 coba diperbaiki oleh plugin ini kapan pun memungkinkan.
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 sebuah argumen field yang menerima ID untuk custom post disebut customPostID dan bukan postID (meskipun itulah yang disebut dalam fungsi WordPress yang dipetakan).
Kemudian, ekspektasinya selalu jelas:
- Field
User.customPostsdapat mengembalikan daftar custom post apa pun, termasuk posts dan pages, danUser.postshanya mengembalikan posts - Field
Root.setFeaturedImageOnCustomPostdapat menambahkan gambar unggulan ke custom post mana pun, itulah mengapa tidak disebutsetFeaturedImageOnPost
Tidak mengelompokkan tag (dan kategori) di bawah satu tipe
Mengapa tipe PostTag (dan sama halnya untuk PostCategory) disebut demikian, alih-alih sekadar Tag?
Karena, saat mengeksekusi query ini (di mana 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
}
}
}Tag yang ditambahkan ke posts tidak akan muncul saat mengambil tag untuk produk, dan sebaliknya (kecuali produk juga menggunakan taksonomi post_tag, tetapi dalam kasus itu juga dapat direpresentasikan dengan tipe PostTag). Ini tidak menjadi masalah besar di WordPress, karena item-item ini dapat dianggap sebagai baris berbeda dari tabel basis data yang sama. Tetapi hal itu penting untuk GraphQL, yang bertipe kuat.
Kemudian, keputusan desain yang baik adalah menjaga entitas-entitas ini terpisah, di bawah tipe mereka sendiri, dan memiliki tag untuk posts yang dikembalikan di bawah tipe PostTag dan, jika plugin kustom mengimplementasikan CPT produknya sendiri, plugin tersebut harus menggunakan tipe ProductTag untuk tag-tagnya.
Memberikan elemen media identitas mereka sendiri
Entitas media di WordPress adalah custom post types, hanya karena 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:
- 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-nilai tetap dari kumpulan tertentu. Misalnya, status sebuah post hanya dapat berupa "publish", "draft", "pending", atau "trash".
Dalam GraphQL, kita dapat memperlakukan ini sebagai enum (alih-alih string), dan membuat tipe enumerasi yang bersesuaian. Mengikuti standar GraphQL, enum harus ditulis dalam huruf besar, 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 menjaga nilai-nilai enum ini dalam huruf kecil:
enum CUSTOM_POST_STATUS {
publish
draft
pending
trash
}Memetakan tipe-tipe tambahan
Blok tidak terlihat langsung dalam diagram basis data WordPress, karena disimpan di wp_posts (tidak ada tabel wp_blocks), tetapi meskipun demikian mereka adalah entitas yang berbeda.
Oleh karena itu, kita tetap dapat memperkenalkan tipe Block untuk memetakannya:
type Post {
blocks: [Block]
}
type Block {
type: String!
attributes: JSONObject
}