๐จ๐ปโ๐ป GraphQL sebagai (semacam) bahasa pemrograman
GraphQL, meskipun memiliki bahasa GraphQL, biasanya tidak disebut sebagai bahasa pemrograman, karena ada begitu banyak hal yang dapat kita lakukan dengan bahasa pemrograman yang tidak dapat kita lakukan dengan GraphQL.
GraphQL biasanya digunakan untuk mengambil data, misalnya untuk merender sebuah website di sisi klien, dan untuk mengubah data, misalnya untuk membuat sebuah post. Dan itu kurang lebih sudah semuanya.
(Penggunaan lain hanyalah kombinasi dari 2 kasus sebelumnya. Misalnya, sebuah API gateway dapat mengambil/mengubah data dari server internal, yang tidak terekspos ke klien.)
Mengakses data di GraphQL:
query PrintPostTitle($postID: ID!)
{
post(by: { id: $postID }) {
title
}
}...memiliki padanan (semacam) ini dalam PHP:
function printPostTitle(int $postID)
{
$post = getPost($postID);
echo $post->title;
}(Semua contoh di bawah ini akan menggunakan PHP sebagai bahasa pemrograman untuk perbandingan.)
Mengubah data di GraphQL:
query UpdatePost($postID: ID!, $title: String!)
{
updatePost(
by: { id: $postID },
input: { title: $title }
) {
title
}
}...memiliki padanan (semacam) ini dalam PHP:
function updatePost(int $postID, string $title)
{
$post = getPost($postID);
$post->update(['title' => $title]);
}Ini sudah cukup karena GraphQL biasanya diakses dari klien (yang dikodekan dalam beberapa bahasa pemrograman, seperti JavaScript, PHP, Java, atau lainnya) yang akan berisi logika tentang apa yang harus dilakukan dengan data. Jadi GraphQL tidak digunakan secara mandiri, melainkan sebagai pendamping bagi yang lain.
Namun jika GraphQL bisa digunakan secara mandiri, maka banyak kasus penggunaan baru dapat diselesaikan hanya menggunakan GraphQL, memungkinkan GraphQL untuk diterapkan ke lingkungan baru dan bertanggung jawab atas tugas-tugas tambahan dalam tumpukan aplikasi.
Namun untuk itu, GraphQL harus mendukung banyak fitur bahasa pemrograman.
Fitur bahasa pemrograman yang didukung GraphQL sangat terbatas. Misalnya, menggunakan direktif @include (atau @skip) dan meneruskan variabel sebagai input dapat dianggap (semacam) logika kondisional:
query PrintPostProperties($postID: ID!, $addContent: Boolean!)
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}Query ini memiliki padanan PHP berikut:
function printPostProperties(int $postID, bool $addContent)
{
$post = getPost($postID);
echo $post->title;
if ($addContent) {
echo $post->content;
}
}Itu kurang lebih sudah semuanya. GraphQL tidak memiliki rekursi, variabel dinamis (di mana nilainya dihitung dan ditetapkan ke variabel saat runtime, bukan sebagai input dalam kamus), penugasan variabel (misalnya: menetapkan output field ke variabel, yang kemudian dapat diberikan sebagai argumen ke field lain), dan lainnya.
Pertimbangkan bagaimana Anda akan mengimplementasikan solusi, hanya menggunakan GraphQL, untuk masalah berikut:
- Buat sebuah webhook yang dipanggil oleh layanan setiap kali pengguna baru mendaftar ke layanan tersebut; pengguna mungkin telah berlangganan newsletter (ditunjukkan oleh field
marketing_optindalam payload webhook); dalam kasus tersebut, webhook harus mendaftarkan email pengguna (dalam fieldemaildi payload webhook) ke daftar Mailchimp.
Apakah Anda pikir itu bisa dilakukan? mudah? sulit? mustahil?
Di Gato GraphQL, kami ingin menyelesaikan masalah ini hanya menggunakan GraphQL. Dan banyak masalah lainnya. Itulah mengapa kami telah berpikir keras tentang cara mendukung karakteristik dari bahasa pemrograman.
Mari kita jelajahi fitur pemrograman apa yang telah kami dukung di server GraphQL kami. Di akhir post ini, kita akan melihat bagaimana kita dapat menyelesaikan masalah tersebut.
Fungsionalitas
Field dalam GraphQL biasanya membawa data, seperti judul, konten, atau data sebuah post. Namun kita juga dapat mengimplementasikan field sebagai "fungsionalitas".
Misalnya, mencetak waktu dalam PHP:
function printTime()
{
echo time();
}...dapat dilakukan dengan field _time di GraphQL:
{
_time
}Perhatikan bahwa fungsi time tidak termasuk dalam tipe apa pun, sehingga field _time pun demikian. Dengan demikian, ini adalah field global, dan dapat diakses di bawah setiap tipe dari skema GraphQL:
{
posts {
_time
}
}Contoh lain dari field fungsionalitas adalah:
_arrayItem_arrayJoin_date_equals_inArray_intAdd_isEmpty_isNull_makeTime_objectProperty_sprintf_strContains_strRegexReplace_strSubstr
Fungsi
Kita dapat memecah unit logika menjadi fungsi-fungsi, dan memiliki satu fungsi yang memanggil fungsi lain:
function printPostProperties(int $postID)
{
$post = getPost($postID);
printPostTitle();
printPostContent();
}
function printPostTitle(Post $post)
{
echo $post->title;
}
function printPostContent(Post $post)
{
echo $post->content;
}Dalam GraphQL, kita dapat dengan cara serupa memecah operasi query (atau mutation) dalam dokumen menjadi beberapa operasi query, dan membuat sebuah operasi "bergantung" pada operasi lain, sehingga operasi tersebut dieksekusi terlebih dahulu:
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"
])
{
# ...
}Dalam query ini, mengeksekusi query GraphQL dengan meneruskan ?operationName=PrintPostProperties ke endpoint akan terlebih dahulu mengeksekusi queries PrintPostTitle dan PrintPostContent, dan baru kemudian PrintPostProperties.
Ini dimungkinkan melalui Multiple Query Execution.
Variabel Dinamis
Kita dapat menghitung nilai dan menetapkannya ke variabel saat runtime. Kemudian, berdasarkan nilai tersebut, kita dapat secara kondisional mengeksekusi beberapa fungsionalitas atau tidak:
function printPostProperties(int $postID)
{
$post = getPost($postID);
echo $post->title;
$addContent = isUserLoggedIn();
if ($addContent) {
echo $post->content;
}
}Dalam GraphQL, kita dapat "mengekspor" nilai di bawah variabel dinamis dalam suatu operasi, dan kemudian membaca nilai ini pada operasi lain:
query ExportAddContent
{
addContent: isUserLoggedIn
@export(as: "addContent")
}
query PrintPostProperties($postID: ID!)
@depends(on: "ExportAddContent")
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}Perhatikan bahwa variabel $addContent, yang menyimpan nilai yang dihitung saat runtime, dibaca tetapi tidak dideklarasikan pada operasi PrintPostProperties, karena ini adalah variabel dinamis.
Mengeksekusi fungsi secara kondisional
Alternatif dari contoh sebelumnya adalah mengelompokkan logika ke dalam fungsi-fungsi, dan kemudian secara kondisional mengeksekusi sebuah fungsi atau tidak tergantung pada nilai variabel dinamis:
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;
}Dalam GraphQL kita dapat menambahkan direktif @include pada operasi:
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"
])
{
# ...
}Sekarang, operasi PrintPostContent hanya akan dieksekusi jika $addContent bernilai true.
Menetapkan variabel, Memasukkannya kembali
Mari kita modifikasi sedikit contoh sebelumnya, di mana kondisi "addContent" terikat pada apakah pengguna sudah login atau belum.
Dalam contoh lain ini, "addContent" bernilai true setiap kali hari ini adalah akhir pekan, yang melibatkan beberapa logika untuk dihitung:
- Dapatkan tanggal hari ini
- Format ke nama hari, dalam huruf kecil
- Periksa apakah itu
"saturday"atau"sunday"
Dalam 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;
}
}Dalam 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)
}
}Dalam operasi ExportAddContent, nilai untuk setiap field yang di-query langsung tersedia untuk field-field di bawahnya, di bawah variabel dinamis $__fieldName. Dengan cara ini output dari sebuah field dapat langsung digunakan sebagai input ke field lain, sudah dalam operasi yang sama.
Ini dimungkinkan berkat Field to Input.
Memodifikasi nilai secara dinamis
Dalam contoh PHP ini, kita memodifikasi nilai sebuah variabel ketika pengguna yang login adalah admin, di mana dalam kasus tersebut konten post ditambahkan tautan untuk mengedit post:
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;
}Dalam GraphQL, kita dapat secara kondisional mengeksekusi satu operasi atau operasi lain, menghasilkan nilai berbeda untuk beberapa field:
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"
])
{
# ...
}Dengan menggunakan direktif @include dan @skip dengan variabel dinamis yang sama sebagai input, operasi RetrieveContentForAdminUser dan RetrieveContentForNonAdminUser bersifat saling eksklusif.
Mengiterasi array
Katakanlah kita ingin mengiterasi item-item dalam sebuah array, dan mengonversi nilai-nilai tersebut menjadi huruf kapital:
function printUserRolesAsUppercase(int $userID)
{
$user = getUser($userID);
foreach ($user->roles as $role) {
echo strtoupper($role);
}
}Dalam GraphQL, kita dapat menggunakan direktif @underEachArrayItem untuk mengiterasi item-item array, dan memberikan masing-masing nilai tersebut ke direktif berikutnya dalam rantai, dalam hal ini @strUpperCase:
query PrintUserRolesAsUppercase($userID: ID!)
{
user(by: { id: $userID }) {
roles
@underEachArrayItem
@strUpperCase
}
}Ini dimungkinkan berkat composable directives.
Operasi CRUD secara massal
CRUD adalah singkatan dari Create, Read, Update dan Delete, ini adalah operasi yang kita terapkan pada sumber daya (post, pengguna, dll).
Membaca secara massal dalam PHP terlihat seperti ini:
function getPostTitles()
{
$posts = getPosts();
foreach ($posts as $post) {
echo $post->title;
}
}Kasus penggunaan ini secara alami terpenuhi oleh GraphQL:
query GetPostTitles
{
posts {
title
}
}Memperbarui secara massal dalam PHP terlihat seperti ini:
function updatePostTitlesAsUppercase()
{
$posts = getPosts();
foreach ($posts as $post) {
$post->update(['title' => strtoupper($post->title)]);
}
}Mengeksekusi pembaruan secara massal di GraphQL biasanya didukung dengan membuat mutation khusus updatePosts, yang mengambil data untuk semua post.
Saya tidak menyukai pendekatan ini, karena secara efektif menggandakan jumlah mutation dalam skema (satu untuk mengubah sumber daya tunggal, satu untuk mengubah beberapa sumber daya), dan kita perlu memelihara logika untuk keduanya:
updatePost+updatePostscreatePost+createPosts- dll
Menurut pendapat saya, pendekatan yang lebih elegan adalah menggunakan nested mutations, di mana mutation Post.update diterapkan pada masing-masing sumber daya yang di-query:
mutation UpdatePostTitlesAsUppercase
{
posts {
title
ucTitle: _strUpperCase(text: $__title)
update(
input: { title: $__ucTitle }
) {
status
post {
title
}
}
}
}Pendekatan yang sama berlaku untuk menghapus sumber daya:
function deletePosts()
{
$posts = getPosts();
foreach ($posts as $post) {
$post->delete();
}
}Dalam GraphQL:
mutation DeletePosts
{
posts {
delete {
status
}
}
}Untuk pembuatan, kita tidak meneruskan sumber daya karena belum ada; sebagai gantinya, kita menyediakan sebuah array dengan input data untuk semua sumber daya yang akan dibuat:
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();
}
}Membuat post secara massal di GraphQL menggunakan satu mutation createPost agak rumit, tetapi tetap bisa dilakukan.
Idenya adalah mengiterasi array dengan input data, menetapkan masing-masing di bawah variabel dinamis $input, dan kemudian mengeksekusi mutation createPost dengan meneruskan input tersebut. Akhirnya kita mendapatkan ID yang dihasilkan dari post yang dibuat di bawah variabel dinamis $createdPostIDs, dan kita mengambil datanya:
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
}
}Mengirim permintaan HTTP (dan fungsi lainnya)
Mengirim permintaan HTTP ke beberapa server web dapat dipenuhi melalui fungsi khusus di PHP, seperti file_get_contents atau curl_exec.
Menggunakan file_get_contents:
$xml = file_get_contents("http://www.example.com/file.xml");Dalam GraphQL, logika untuk mengeksekusi permintaan HTTP dapat dipenuhi melalui field fungsionalitas, seperti _sendHTTPRequest:
query {
_sendHTTPRequest(input: {
url: "http://www.example.com/file.xml",
method: GET
}) {
xml: body
}
}Konsep yang sama berlaku untuk fungsionalitas apa pun.
Misalnya, kita mengakses nilai konstanta di PHP seperti ini:
$mailchimpUsername = constant('MAILCHIMP_API_CREDENTIALS_USERNAME');Kita dapat mengimplementasikan field fungsionalitas yang bersesuaian di GraphQL:
{
mailchimpUsername: _env(name: "MAILCHIMP_API_CREDENTIALS_USERNAME")
}Menyelesaikan tantangan hanya menggunakan GraphQL
Dengan semua fitur bahasa pemrograman yang baru saja kita bahas, kita sekarang dapat menggunakan hanya GraphQL untuk menyelesaikan masalah yang diajukan sebelumnya:
- Buat sebuah webhook yang dipanggil oleh layanan setiap kali pengguna baru mendaftar ke layanan tersebut; pengguna mungkin telah berlangganan newsletter (ditunjukkan oleh field
marketing_optindalam payload webhook); dalam kasus tersebut, webhook harus mendaftarkan email pengguna (dalam fieldemaildi payload webhook) ke daftar Mailchimp.
Solusinya adalah menggunakan persisted query GraphQL sebagai webhook, dengan query ini:
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"
}
}
})
}Dalam solusi ini, operasi MaybeCreateContactOnMailchimp, yang mengeksekusi permintaan HTTP terhadap API Mailchimp, akan dieksekusi secara kondisional, tergantung pada nilai field marketing_optin.
(Baca blog post ๐จ๐ปโ๐ซ GraphQL query to automatically send the newsletter subscribers from InstaWP to Mailchimp untuk melihat cara kerja query ini.)
GraphQL lebih powerful dari yang Anda kira!
GraphQL dapat digunakan untuk jauh lebih dari sekadar mengambil dan mengubah data... Mengadaptasi data, memodifikasi output secara dinamis, menyesuaikan konten untuk konteks yang berbeda, membuat API gateway hanya dengan beberapa baris kode, dan masih banyak lagi.
Dengan mendukung fitur-fitur bahasa pemrograman, kita dapat menyelesaikan tantangan di atas hanya menggunakan GraphQL, dan menghindari penerapan klien yang harus menyertainya. Kita kemudian menyederhanakan tumpukan aplikasi: Lebih sedikit bagian yang bergerak, lebih sedikit kompleksitas, lebih sedikit kode untuk di-debug, lebih sedikit teknologi yang harus dihadapi.
GraphQL keren ๐ค