🕸 GraphQL สามารถปรับปรุง WordPress ได้อย่างไรและที่ไหน โดยเสริม REST API
อัปเดต 01/05/2024: ดู Gato GraphQL vs WP REST API การเปรียบเทียบ
เมื่อสุดสัปดาห์ที่ผ่านมา ฉันได้เผยแพร่บทความบล็อก 🦸🏿♂️ Gato GraphQL ตอนนี้ถูก transpile จาก PHP 8.0 เป็น 7.1
หลังจาก แชร์บทความบน Reddit's /r/php ชุมชนได้เริ่มการอภิปรายอย่างคึกคักเกี่ยวกับความคุ้มค่าของการใช้ GraphQL ใน WordPress ความแตกต่างจาก WP REST API และความสมเหตุสมผลของการนำ API อีกตัวมาสู่ WordPress
ฉันคิดว่าความคิดเห็นส่วนใหญ่ถูกต้อง แต่บางส่วนขาดข้อมูลสำคัญ GraphQL ไม่ได้เป็นแค่อินเทอร์เฟซ แต่ยังเป็นการนำไปใช้งานด้วย ซึ่งหมายความว่า GraphQL servers ที่แตกต่างกันจากผู้ให้บริการต่างกัน อาจออกแบบมาเพื่อให้ความสำคัญกับคุณลักษณะที่แตกต่างกัน ดังนั้น เราจึงไม่สามารถมีความคาดหวังที่เป็นหนึ่งเดียวเสมอไปว่า GraphQL เสนออะไร หรือเข้าใจอย่างสมบูรณ์ว่า GraphQL engine ทำงานอย่างไร
ตัวอย่างเช่น ประสบการณ์ GraphQL ใน WordPress และใน Laravel จะแตกต่างกัน เช่นเดียวกับประสบการณ์ที่ได้รับจาก servers ต่างกัน ไม่ว่าจะเป็น WPGraphQL หรือ Gato GraphQL
บทความนี้เป็นความคิดเห็นของฉันเกี่ยวกับเรื่องนี้ โดยตอบสนองต่อความคิดเห็นหลายๆ ข้อจากโพสต์ใน Reddit
GraphQL vs WP REST API
[ความคิดที่แย่มาก] ที่จะมี GraphQL API บน WordPress ซึ่งใช้ REST API ของตัวเองอยู่แล้ว ใช้ REST API ไปเลยดีกว่า [แหล่งที่มา]
ทั้ง REST API และ GraphQL มีวัตถุประสงค์เดียวกัน: ให้ข้อมูลที่แอปพลิเคชันต้องการ อย่างไรก็ตาม วิธีการบรรลุเป้าหมายนี้แตกต่างกัน: ในขณะที่ REST มี endpoints ที่กำหนดไว้ล่วงหน้าซึ่งให้ข้อมูลชุดเฉพาะ GraphQL สามารถให้ข้อมูลที่ต้องการได้อย่างแม่นยำ
ความแตกต่างของพฤติกรรมนี้อาจส่งผลโดยตรงต่อประสิทธิภาพของแอปพลิเคชัน ด้วย REST หากเราต้องดึงรายการโพสต์พร้อมข้อมูลบางอย่างจากผู้เขียนแต่ละโพสต์ จะต้องส่งคำขอเพิ่มเติม อาจเป็น 1 คำขอเพิ่มเติมสำหรับข้อมูลผู้เขียนทั้งหมด หรือ 1 คำขอเพิ่มเติมต่อผู้เขียนแต่ละคน ในระหว่างนั้น ผู้เยี่ยมชมเว็บไซต์อาจรอการแสดงผลหน้า
GraphQL ปรับปรุงสถานการณ์นี้ เนื่องจากเราสามารถดึงข้อมูลโพสต์และผู้เขียนทั้งหมดได้ในคำขอเดียว และการแสดงผลหน้าเว็บจะเร็วขึ้น:
{
posts {
id
title
excerpt
date
url
author {
id
name
url
}
}
}แม้ว่า WordPress จะมี REST API อยู่แล้ว แต่ก็ไม่ได้หมายความว่ามันเป็นเครื่องมือที่เหมาะสมที่สุดสำหรับทุกงานเสมอไป แน่นอน เราสามารถใช้มันได้เสมอ แต่หากเราสามารถเข้าถึง GraphQL ได้ด้วย เราก็สามารถตัดสินใจใช้ API นี้เมื่อมันให้ข้อได้เปรียบเหนือ REST และเราจะได้ผลลัพธ์ที่ดีกว่า
การตั้งค่าเริ่มต้นที่ยากของ GraphQL + การต้องเขียน resolvers
มีข้อโต้แย้งที่ชัดเจนว่าการตั้งค่าเริ่มต้นสำหรับ GraphQL สูงกว่า REST อย่างเป็นเลขชี้กำลัง คุณพูดถูกที่ว่าต้องตั้งค่าการเชื่อมโยง [แหล่งที่มา]
และ...
สิ่งที่คุณและเกือบทุกคนบนเว็บละเว้นคือ เพื่อให้รูปแบบ API นี้ทำงานได้ คุณต้องเขียน parser (resolvers + types) ซึ่งนำปัญหามากมายที่ไม่มีใน REST มาด้วย [แหล่งที่มา]
ความคิดเห็นเหล่านี้ไม่ถูกต้องทั้งหมด เพราะทั้ง WPGraphQL และ Gato GraphQL ได้ทำการ map โมเดลข้อมูล WordPress ลงใน GraphQL schema แล้ว (WPGraphQL ทำครบทั้งหมด plugin ของฉันทำส่วนใหญ่)
ดังนั้น หลังจากที่คุณติดตั้ง plugin ใดก็ตามเหล่านี้ คุณสามารถเริ่มดึงข้อมูลสำหรับแอปพลิเคชันของคุณได้ทันที โดยไม่ต้องสร้าง resolver ใดๆ หรือต้องตั้งค่าการเชื่อมโยงระหว่าง entities
เป็นความจริงที่ว่า เพื่อดึงข้อมูลกำหนดเองจาก entities ของแอปพลิเคชัน (เช่น จาก CPTs) สิ่งเหล่านี้จำเป็นต้อง map ผ่าน resolvers และคุณจะต้องทำเอง แต่นี่ไม่ต่างจาก REST: หากคุณต้องการข้อมูลกำหนดเองจาก CPT คุณจะต้องสร้าง REST endpoint เพื่อดึงข้อมูลกำหนดเองนั้น Custom endpoint ก็คือ resolver เช่นกัน
ดังนั้น เกี่ยวกับความจำเป็นของ resolvers REST และ GraphQL API มีความคล้ายคลึงกันมาก
จากการเรียกดูเว็บไซต์และเอกสาร จะให้ความรู้สึกว่า GraphQL ต้องใช้ความพยายามในการตั้งค่ามากกว่า ดังนั้น จึงมีความจริงในการสันนิษฐานนี้
ฉันเชื่อว่ามีเหตุผลบางประการสำหรับเรื่องนี้ ประการแรก GraphQL เกี่ยวข้องกับ (อย่างน้อย) สองส่วน:
- แนวคิดว่ามันคืออะไร และทำงานอย่างไร
- servers ที่ให้การนำไปใช้งานจริง
เมื่อเรียกดูเอกสารสำหรับ GraphQL เช่น เว็บไซต์ทางการ graphql.org มันมุ่งเน้นที่แนวคิดเบื้องหลัง GraphQL โดยเจาะลึกใน resolvers ว่าคืออะไรและทำไมจึงจำเป็น
สิ่งนี้มีประโยชน์เมื่อคุณกำลังสร้างแอปพลิเคชันจากศูนย์ เช่น ถ้าใช้ Laravel และ Lighthouse ในกรณีนั้น คุณจำเป็นต้องเขียนโค้ด resolvers ของคุณ (แต่คุณก็ต้องสร้าง REST endpoints ของคุณเช่นกัน)
อย่างไรก็ตาม WordPress เป็นแอปพลิเคชันอยู่แล้ว และ WPGraphQL กับ Gato GraphQL คือโซลูชัน plugin ทั้งสองนี้ได้สร้าง resolvers ให้เราแล้ว ดังนั้นเราไม่ต้องกังวลเรื่องนั้น (คล้ายกับที่ WP REST API ให้ชุด endpoints เริ่มต้น ดังนั้นเราไม่ต้องกังวล)
นอกจากนี้ GraphQL มุ่งเน้นนักพัฒนาเป็นศูนย์กลาง และเอกสารดูเหมือนพูดตรงๆ กับนักพัฒนา นักพัฒนาสร้าง resolvers ฝั่ง server และนักพัฒนาใช้ resolvers เหล่านั้นด้วย queries กำหนดเองฝั่ง client เนื่องจากการสร้าง resolvers เป็นงานของนักพัฒนา มันจึงปรากฏขึ้นอย่างเป็นธรรมชาติและบ่อยครั้ง
สำหรับ REST ความคาดหวัง (ฉันเชื่อ) คือ endpoint ที่ให้ข้อมูลที่ต้องการจะมีอยู่แล้ว (ตามที่ WP REST API จัดส่ง) ถ้าไม่มี เราถึงจะต้องกังวลเรื่องการตั้งค่า custom endpoint ดังนั้น จึงให้ความสำคัญกับการสร้าง resolvers สำหรับ REST น้อยกว่า
ดังนั้น ทั้ง REST และ GraphQL ให้ข้อมูลที่จำเป็น แต่ในขณะที่ REST ส่งเสริมแนวทางแบบ static ที่ endpoints ควรมีอยู่แล้ว และเราจะกังวลเมื่อไม่มีเท่านั้น GraphQL ส่งเสริมแนวทางแบบ dynamic ที่ทุก query ถูกสร้างแบบกำหนดเอง และเราสามารถเขียนโค้ด resolver ที่สมบูรณ์แบบสำหรับมัน
ดังนั้น ในที่สุด ไม่มีความแตกต่างพื้นฐานระหว่าง REST และ GraphQL เพียงแค่การตีความที่แตกต่างกันว่าพวกเขาต้องตอบสนองความต้องการอย่างไร
ช่องโหว่ + ข้อควรพิจารณาด้านความปลอดภัยใน GraphQL
เราจะเห็นช่องโหว่ขนาดใหญ่จาก GraphQL สักวันหนึ่ง เพราะการเขียน interpreters ที่ปลอดภัยนั้นยากจริงๆ [แหล่งที่มา]
และ...
WordPress มีขนาดใหญ่มากอยู่แล้วจนเป็นเป้าหมายขนาดใหญ่อยู่แล้ว การเพิ่ม plugin ใดๆ จะเพิ่มความเสี่ยงอย่างมาก และ plugin ที่เสนอจะเปิดเผย WordPress ทั้งหมดอย่างแท้จริง รวมถึง โค้ดตัวอย่างมากมายสำหรับการข้ามผ่านโมเดลความปลอดภัย ถือเป็นสิ่งที่ฉันไม่ยอมรับ output ที่ไม่ใช่ theme-driven ควรถูกจำกัดให้มากที่สุดเท่าที่จะเป็นไปได้ (ไม่มีอยู่ถ้าฉันไม่ขอ) นอกเหนือจากสิ่งที่จำเป็นต้องเปิดเผยโดยสิ้นเชิง ฉันหวังว่าสิ่งนี้จะไม่ถูกรวมเข้าใน core [แหล่งที่มา]
GraphQL นำความเสี่ยงด้านความปลอดภัยเพิ่มเติมที่เราต้องจัดการ ฉันเห็นด้วยอย่างเต็มที่กับความรู้สึกนี้
แต่ฉันไม่คิดว่ามันเป็นปัญหาที่ขัดขวางถึงขนาดป้องกันการรวม GraphQL ไว้ใน WP core ยิ่งไปกว่านั้น ฉันไม่คิดว่ามันยากจริงๆ ที่จะแก้ไข
สิ่งที่จำเป็นคือให้ GraphQL server ใช้กลไกความปลอดภัยที่มีอยู่ของ WordPress จากนั้นนักพัฒนาจะใช้กลไกเหล่านี้ เพื่อให้แน่ใจว่า field บางอย่างสามารถเข้าถึงได้โดยผู้ใช้ที่เหมาะสมเท่านั้น:
- ผู้ใช้ล็อกอินอยู่หรือไม่?
- ผู้ใช้เป็น admin หรือไม่?
- ผู้ใช้มี role หรือ capability บางอย่างหรือไม่?
- ผู้ใช้เป็นผู้เขียนโพสต์หรือไม่?
เพื่อตอบสนองข้อเสนอนี้ Gato GraphQL เสนอ Access Control Lists เพื่อให้เราสามารถกำหนดว่าใครสามารถเข้าถึง field และ directive แต่ละรายการได้ และโดยการกำหนดค่า
ในบางครั้ง การใช้ ACL เพียงอย่างเดียวไม่เพียงพอ และ GraphQL server จำเป็นต้องให้มาตรการความปลอดภัยเพิ่มเติม ฉันจะอธิบายสิ่งที่ฉันกำลังทำอยู่ตอนนี้สำหรับ v0.8 ที่กำลังจะมาถึงของ Gato GraphQL
Field posts (เพื่อดึงข้อมูลโพสต์) ไม่ต้องการการยืนยันตัวตน ผู้ใช้ใดก็ตามสามารถเข้าถึงได้ ไม่ว่าจะล็อกอินอยู่หรือไม่ ดังนั้น เพื่อเหตุผลด้านความปลอดภัย มันดึงเฉพาะโพสต์ที่เผยแพร่แล้วเท่านั้น
แต่มีสถานการณ์ที่เราต้องดึงโพสต์ draft/pending/trashed ด้วย เช่น:
- สำหรับการสร้างเว็บไซต์แบบ static ซึ่งดำเนินการโดย admin ที่มีสิทธิ์เข้าถึงข้อมูลทั้งหมดจากไซต์
- สำหรับผู้เขียนโพสต์ เพื่อแสดงรายการโพสต์ draft ทั้งหมดเพื่อให้พวกเขาสามารถแก้ไขต่อได้
จากนั้น ฉันคิดรูปแบบดังต่อไปนี้ เพื่อดึงโพสต์ จะมี 3 fields:
posts: เปิดให้ทุกคน สามารถดึงเฉพาะโพสต์ที่เผยแพร่แล้วmyPosts: เปิดให้ทุกคน ดึงเฉพาะโพสต์จากผู้ใช้ที่ล็อกอินอยู่ โดยมีสถานะใดก็ได้ (published/draft/pending/trashed)postsForAdmin: เฉพาะ admin เท่านั้นที่เข้าถึงได้ ดึงโพสต์ใดๆ ที่มีสถานะใดก็ได้
จากนั้น postsForAdmin ถูกปิดใช้งานโดยค่าเริ่มต้น ดังนั้นจะไม่ปรากฏใน GraphQL schema เว้นแต่ admin จะเปิดใช้งานอย่างชัดเจน (และส่วนใหญ่แล้ว จะเปิดใช้งานสำหรับการสร้างไซต์แบบ static เท่านั้น)
อีกสถานการณ์หนึ่งคือเมื่อ field บางอย่างสามารถดึงทั้งข้อมูลสาธารณะและข้อมูลส่วนตัว ตัวอย่างเช่น field option ดึงข้อมูลจากตาราง wp_options บางรายการเป็นสาธารณะ (เช่น blogname) ในขณะที่บางรายการไม่ใช่ (เช่น admin_email)
สถานการณ์ที่คล้ายกันคือการดึงค่า meta ผ่าน fields Post.metaValue, User.metaValue และอื่นๆ ตัวอย่างเช่น user meta รวมถึงรายการ wp_capabilities ซึ่งแน่นอนว่าเป็นส่วนตัว ในขณะที่ description เป็นสาธารณะ และ last_name อาจเป็นสาธารณะหรือส่วนตัวขึ้นอยู่กับแอปพลิเคชัน
เพื่อให้การเข้าถึงข้อมูลนี้ปลอดภัย plugin จะเปิดใช้งานการระบุว่ารายการใดสามารถ query ได้ผ่าน allow/denylist ในหน้าการตั้งค่า โดยรับทั้งรายการเต็มหรือ regex:

จากนั้น การ query option ที่อนุญาตจะทำงาน ในขณะที่ option ที่ปฏิเสธจะคืนค่า null:
{
# This option is allowed
siteName: optionValue(name: "blogname")
# This optionValue is not allowed
adminEmail: optionValue(name: "admin_email")
}ด้วยมาตรการความปลอดภัยที่เหมาะสมจาก GraphQL server และสามัญสำนึกของนักพัฒนา การสร้าง GraphQL API ที่ปลอดภัยไม่ควรเป็นเรื่องยาก
GraphQL ทำให้ DB ล่ม
GraphQL เป็น syntax ที่สมบูรณ์แบบที่อนุญาตให้แสดง relational queries เชิงลึกได้ ดังนั้น สำหรับ ecosystem อย่าง WordPress ที่ความสามารถในการขยายของโมเดลข้อมูลมาจาก entity-attribute-value pattern สิ่งนี้แปลเป็นความเครียดที่น่าเหลือเชื่อบน database ซึ่งอาจทำให้ไซต์ของคุณไม่ตอบสนองหาก GraphQL query ลึก ซับซ้อน หรือ recursive WordPress มีชื่อเสียงอยู่แล้วที่สามารถทำให้ MySQL/MariaDB instance ล่มได้ ดังนั้นการเพิ่ม GraphQL อาจทำให้แย่ลงมากถ้า queries ไม่ถูกเขียน ยืนยันตัวตน และจำกัดอัตราอย่างเหมาะสม [แหล่งที่มา]
การทำให้ DB ล่มเป็นความกังวลอย่างจริงจังสำหรับ GraphQL servers ฉันจะอธิบายว่า Gato GraphQL พยายามหลีกเลี่ยงสถานการณ์นี้อย่างไร
Gato GraphQL หลีกเลี่ยงปัญหา N+1 ไม่ให้เกิดขึ้น โดยการออกแบบสถาปัตยกรรมตั้งแต่ต้น มันทำได้โดยให้ engine รับผิดชอบในการโหลด entities จาก database ไม่ใช่นักพัฒนา
เมื่อแก้ไข connections ใน resolver ค่าที่คืนมาคือ ID (หรือรายการ IDs) ของ object ไม่ใช่ object เอง ตัวอย่างเช่น การดึง author ของ custom post ทำได้แบบนี้:
class CustomPostFieldResolver extends AbstractDBDataFieldResolver
{
private CustomPostUserTypeAPIInterface $customPostUserTypeAPI;
public function getClassesToAttachTo(): array
{
return [
CustomPostFieldInterfaceResolver::class,
];
}
public function getSchemaFieldType(string $fieldName): ?string
{
return match($fieldName) {
'author' => SchemaDefinition::TYPE_ID,
default => null,
};
}
public function resolveValue(
TypeResolverInterface $typeResolver,
object $customPost,
string $fieldName,
array $fieldArgs = []
): mixed {
switch ($fieldName) {
case 'author':
return $this->customPostUserTypeAPI->getAuthorID($customPost);
}
return null;
}
public function resolveFieldTypeResolverClass(
TypeResolverInterface $typeResolver,
string $fieldName
): ?string {
switch ($fieldName) {
case 'author':
return UserTypeResolver::class;
}
return null;
}
}โดยมี ID ของ DB entity จาก resolveValue และประเภทของ object จาก resolveFieldTypeResolverClass (แสดงผ่าน class UserTypeResolver) GraphQL engine สามารถโหลดข้อมูลสำหรับ object ได้
เพื่อโหลดข้อมูล engine ใช้อัลกอริทึมที่มีประสิทธิภาพสูงมาก: มี time complexity O(n) โดยที่ n คือจำนวน types ใน query ไม่ใช่จำนวน nodes
อัลกอริทึมบรรลุประสิทธิภาพนี้เพราะมันไม่ traverse graph แต่ แปลงโครงสร้างข้อมูลเป็น stack ของ components ซึ่งง่ายต่อการแก้ไขมากกว่า ("graph" ใน GraphQL เป็นแนวคิด ไม่ใช่การนำไปใช้งานจริง)
จากนั้น แม้ว่า query จะมีหลายระดับ แต่ละระดับดึง entities มากมาย อัลกอริทึมก็ยังสามารถรับมือได้ดี ตัวอย่างเช่น ไม่มีผลกระทบมากเมื่อรัน query ต่อไปนี้ ซึ่งมีความลึก 10 ระดับ:
{
posts(pagination: { limit: 10 }) {
excerpt
title
url
author {
name
url
posts(pagination: { limit: 10 }) {
title
tags(pagination: { limit: 10 }) {
slug
url
posts(pagination: { limit: 10 }) {
title
comments(pagination: { limit: 10 }) {
content
date
author {
name
posts(pagination: { limit: 10 }) {
title
url
comments(pagination: { limit: 10 }) {
content
date
author {
name
username
url
}
}
}
}
}
}
}
}
}
}
}ข้อยกเว้นของประสิทธิภาพนี้คือเมื่อดึงค่า meta ผ่าน Post.metaValue, User.metaValue, Comment.metaValue, PostTag.metaValue และ PostCategory.metaValue (และ field metaValues ด้วย) นั่นเป็นเพราะฟังก์ชัน WordPress (get_post_meta, get_user_meta ฯลฯ) ดึงข้อมูลสำหรับ 1 ID ต่อครั้ง ซึ่งหมายความว่าแต่ละ entity จะต้องเรียก database เพื่อดึงค่า meta ของมัน ส่งผลให้การแก้ไขค่า meta ขยายตาม number of nodes ไม่ใช่ number of types (ความคิดเห็นในโพสต์ต้นฉบับพูดถูกต้องในเรื่องนี้)
เพื่อป้องกันผู้ไม่ประสงค์ดีจากการใช้และใช้งาน meta fields ในทางที่ผิด Gato GraphQL (ใน v0.8) จะปล่อย fields เหล่านี้พร้อมปิดใช้งานโดยค่าเริ่มต้น จากนั้น admin ต้องเปิดใช้งานอย่างชัดเจน และในขณะทำเช่นนั้น สามารถวาง fields เหล่านี้ภายใต้ Access Control List ดังนั้น DB จะไม่มีความเสี่ยงจากการโจมตีเลย
Rate limiting เป็นความคิดที่ดีเช่นกัน ฉันวางแผนที่จะ รองรับมันสำหรับ release ที่กำลังจะมา
และแล้วก็มีการวิเคราะห์และกำหนดข้อจำกัดความซับซ้อนของ query (เช่น ลึกกี่ระดับ) GraphQL server แก้ไข query ด้วย time complexity O(n) ดังนั้นจึงไม่มีอันตรายมากนักเกี่ยวกับการวนซ้ำ อย่างไรก็ตาม query เดียวยังสามารถดึงข้อมูลจำนวนไม่จำกัดจาก DB และนั่นคือสิ่งที่เราอาจต้องการหลีกเลี่ยง
ตัวอย่างเช่น query ง่ายๆ นี้จะนำข้อมูลจำนวนมากมาในคำขอเดียว (ไซต์ demo ของฉันมีเพียงไม่กี่ร้อยระเบียน ดังนั้นฉันสามารถแสดงการรัน query ได้):
{
posts000: posts(pagination: { limit: 100 }) {
...PostFields
}
posts100: posts(pagination: { limit: 100, offset: 100 }) {
...PostFields
}
posts200: posts(pagination: { limit: 100, offset: 200 }) {
...PostFields
}
posts300: posts(pagination: { limit: 100, offset: 300 }) {
...PostFields
}
posts400: posts(pagination: { limit: 100, offset: 400 }) {
...PostFields
}
posts500: posts(pagination: { limit: 100, offset: 500 }) {
...PostFields
}
posts600: posts(pagination: { limit: 100, offset: 600 }) {
...PostFields
}
posts700: posts(pagination: { limit: 100, offset: 700 }) {
...PostFields
}
posts800: posts(pagination: { limit: 100, offset: 800 }) {
...PostFields
}
posts900: posts(pagination: { limit: 100, offset: 900 }) {
...PostFields
}
}
fragment PostFields on Post {
id
title
content
date
}ดังที่เห็นได้ query ไม่จำเป็นต้องซ้อนกันเพื่อสร้างปัญหา ดังนั้น การวิเคราะห์ความซับซ้อนของ query จึงเป็นเรื่องที่ยุ่งยาก ซึ่งต้องการการปรับแต่งละเอียดเพื่อให้เป็นประโยชน์
ฉันหวังที่จะรองรับการวิเคราะห์ query ด้วย แต่มันไม่ได้อยู่ในรายการลำดับความสำคัญสูงของฉัน เพราะด้วยการรวมกันของ features อื่นๆ (เช่น persisted queries หรือ custom endpoints ควบคู่กับ Access Control Lists) เราสามารถกันผู้ไม่ประสงค์ดีออกได้แล้ว และเราเองก็จะไม่ (ไม่ควร!) ใช้บริการ GraphQL ของเราเองในทางที่ผิด