บล็อก

💁🏽‍♂️ เหตุใดจึงต้องรองรับ CMS-agnosticism, Gato GraphQL ถูกแบ่งออกเป็น ~90 แพ็กเกจ และข้อดีข้อเสียของแนวทางนี้

Leonardo Losoviz
โดย Leonardo Losoviz ·

สัปดาห์ที่แล้วผมได้เผยแพร่บทความ 💁🏻‍♀️ ทำไม Gato GraphQL จึงต้องการ Monorepo และถูกปรับแต่งอย่างไร ซึ่งอธิบายว่า GatoGraphQL/GatoGraphQL monorepo ที่โฮสต์โค้ดสำหรับ Gato GraphQL สามารถจัดการโค้ดเบสของปลั๊กอินได้อย่างมีประสิทธิภาพอย่างไร

ผมแชร์บทความนี้บน Reddit และได้รับความคิดเห็นดังต่อไปนี้:

บทความของ OP และบทความที่ลิงก์ถึง ดูเหมือนจะสื่อว่า monorepo เป็นสิ่งที่ยิ่งใหญ่ที่สุดนับตั้งแต่ขนมปังสไลซ์

บทความที่น่าสนใจกว่าควรอธิบายว่าทำไมคุณถึงคิดว่า CMS-agnosticism จำเป็นต้องแบ่งทุกอย่างออกเป็นแพ็กเกจเล็กๆ ของตัวเอง และทำไมคุณถึงคิดว่าแต่ละแพ็กเกจในกว่า 200 แพ็กเกจจำเป็นต้องอยู่ใน repo ของตัวเองตั้งแต่แรก

นี่เป็นคำถามที่น่าสนใจ ดังนั้นผมจึงตัดสินใจเขียนบทความนี้เพื่ออธิบายเพิ่มเติม

แต่ก่อนอื่น ผมจะกล่าวถึงสองหัวข้อที่เกี่ยวข้อง: จำนวนแพ็กเกจที่ปลั๊กอินต้องการจริงๆ และเหตุใดผมจึงอ้างว่า GraphQL server ที่อยู่เบื้องหลังนั้นเป็น CMS-agnostic

จำนวนแพ็กเกจที่ประกอบเป็นปลั๊กอิน

แม้ว่าผมจะพูดถึงแพ็กเกจ PHP มากกว่า 200 แพ็กเกจ แต่นั่นสำหรับ monorepo สำหรับปลั๊กอินนั้น จริงๆ แล้วน้อยกว่านั้นมาก

GatoGraphQL/GatoGraphQL monorepo ครอบคลุม 5 โปรเจกต์:

  1. PoP ไลบรารีโมเดลคอมโพเนนต์ฝั่งเซิร์ฟเวอร์ (เหมือน React แต่สำหรับ back-end)
  2. GraphQL by PoP GraphQL server ที่เป็น CMS-agnostic สำหรับ PHP
  3. Gato GraphQL
  4. site builder (WIP)
  5. Wassup ธีมเว็บไซต์ที่อิงจาก site builder (WIP)

การโฮสต์โปรเจกต์เหล่านี้ใน monorepo ช่วยให้การทำงานง่ายขึ้นเนื่องจากมีการพึ่งพากันระหว่างโปรเจกต์:

  • GraphQL by PoP อิงจาก PoP
  • Gato GraphQL อิงจาก GraphQL by PoP
  • site builder ใช้ไลบรารีโมเดลคอมโพเนนต์เป็น engine (คล้ายกับที่ Gatsby ใช้ GraphQL)
  • Wassup อิงจาก site builder

สำหรับโค้ดของทั้ง 5 โปรเจกต์ GatoGraphQL/GatoGraphQL มีแพ็กเกจ PHP มากกว่า 200 แพ็กเกจ สำหรับ Gato GraphQL นั้น "เพียงแค่" 91 แพ็กเกจ และ GraphQL by PoP ซึ่งเป็น GraphQL server เบื้องหลัง มี "เพียงแค่" 98 แพ็กเกจ

(Gato GraphQL plugin ต้องการแพ็กเกจน้อยกว่า GraphQL server เบื้องหลัง เนื่องจากบางแพ็กเกจ เช่น Google Translate @strTranslate directive ยังไม่ได้ถูกเพิ่มเข้าไปในปลั๊กอิน)

GraphQL by PoP เป็น CMS-agnostic อย่างไร? แตกต่างจาก webonyx อย่างไร?

ผมได้กล่าวว่า GraphQL by PoP เป็น CMS-agnostic แต่หมายความว่าอะไร?

ในความเป็นจริง webonyx/graphql-php ก็เป็น CMS-agnostic เช่นกัน แล้วทั้งสองต่างกันอย่างไร?

webonyx/graphql-php เป็น CMS-agnostic ในแง่ที่ว่ามันเป็นแพ็กเกจที่แจกจ่ายผ่าน Composer ซึ่งมีเฉพาะโค้ด PHP "vanilla" เท่านั้น อย่างไรก็ตาม มันไม่ใช่ GraphQL server ที่สมบูรณ์ในตัวเอง แต่เป็นการ implement GraphQL specification ใน PHP เพื่อฝังอยู่ใน GraphQL server ใดๆ ใน PHP

ส่วน GraphQL server เหล่านี้ที่ implement อย่าง Lighthouse หรือ WPGraphQL ไม่ได้เป็น CMS-agnostic เราไม่สามารถรัน Lighthouse บน WordPress หรือรัน WPGraphQL บน Laravel ได้

ในแง่นี้เองที่ GraphQL by PoP เป็น CMS-agnostic: มันคือ GraphQL server "เกือบสมบูรณ์" ที่เกือบพร้อมจะรันกับ CMS หรือ framework ใดก็ได้ ไม่ว่าจะเป็น Laravel, WordPress, หรืออื่นๆ (เพื่อความกระชับ ต่อจากนี้ เมื่อผมพูดว่า "CMS" หมายถึง "CMS หรือ framework")

เพื่อทำให้สมบูรณ์สำหรับ CMS ใดๆ GraphQL server ยังคงต้องการโค้ดที่กำหนดเองสำหรับ CMS นั้น ผ่านแพ็กเกจที่สอดคล้องกัน

ตอนนี้ผมจะตอบคำถามในความคิดเห็น

ทำไมแต่ละแพ็กเกจจึงต้องอยู่ใน repo ของตัวเอง

เพราะ Packagist (registry ของแพ็กเกจ PHP ของ Composer) ต้องการให้ระบุ URL ของ repository สำหรับการเผยแพร่/แจกจ่ายแพ็กเกจ

(อนึ่ง บทความของผม Hosting all your PHP packages together in a monorepo ที่เผยแพร่เมื่อสัปดาห์ที่แล้วเช่นกัน พูดถึงประเด็นนี้)

ทำไม CMS-agnosticism จึงต้องการแบ่งทุกอย่างออกเป็นแพ็กเกจเล็กๆ ของตัวเอง

มีเหตุผลหลายประการ

ให้ CMS inject โค้ดของตัวเองได้

เป็นไปไม่ได้ที่จะสร้าง GraphQL server ที่ทำงานได้ทุกที่ โดยใช้โค้ด PHP เหมือนกัน 100%

ตัวอย่างเช่น เพื่อให้โค้ดบางส่วนสามารถแก้ไขค่าของตัวแปรบางอย่างในที่อื่น WordPress พึ่งพา filter hooks, Symfony ใช้ EventDispatcher component, และ Laravel มีระบบ events and listeners ของตัวเอง โค้ด PHP สำหรับทั้ง 3 วิธีที่แตกต่างกันนี้ก็จะแตกต่างกันด้วย

นี่คือจุดที่แนวทางการแบ่งโค้ดออกเป็นแพ็กเกจละเอียดเข้ามามีบทบาท แทนที่จะมี solution สำหรับ events and listeners เป็นส่วนหนึ่งของแอปพลิเคชัน มันจะถูก inject เข้าไปในแอปพลิเคชันผ่านแพ็กเกจ และแพ็กเกจนี้จะมีโค้ดที่เฉพาะเจาะจงกับ CMS

เพื่อให้สิ่งนี้ทำงานได้ ทุกฟังก์ชันจะต้องถูกแบ่งออกเป็น 2 แพ็กเกจ:

  • แพ็กเกจ CMS-agnostic ที่มี business logic ทั้งหมด โดยใช้เฉพาะโค้ด PHP "vanilla" แพ็กเกจนี้จะมี contracts ที่แพ็กเกจ CMS-specific ต้องปฏิบัติตาม
  • แพ็กเกจ CMS-specific ที่ปฏิบัติตาม contracts สำหรับ CMS นั้น

ตัวอย่างเช่น GraphQL by PoP มีแพ็กเกจ hooks ที่มี contract ดังต่อไปนี้:

interface HooksAPIInterface
{
  public function addFilter(string $tag, callable $function_to_add, int $priority = 10, int $accepted_args = 1): void;
  public function removeFilter(string $tag, callable $function_to_remove, int $priority = 10): bool;
  public function applyFilters(string $tag, mixed $value, mixed ...$args): mixed;
  public function addAction(string $tag, callable $function_to_add, int $priority = 10, int $accepted_args = 1): void;
  public function removeAction(string $tag, callable $function_to_remove, int $priority = 10): bool;
  public function doAction(string $tag, mixed ...$args): void;
}

จากนั้น แพ็กเกจ hooks-wp ปฏิบัติตาม contract สำหรับ WordPress:

class HooksAPI implements HooksAPIInterface
{
  public function addFilter(string $tag, callable $function_to_add, int $priority = 10, int $accepted_args = 1): void
  {
    \add_filter($tag, $function_to_add, $priority, $accepted_args);
  }
  public function removeFilter(string $tag, callable $function_to_remove, int $priority = 10): bool
  {
    return \remove_filter($tag, $function_to_remove, $priority);
  }
  public function applyFilters(string $tag, mixed $value, mixed ...$args): mixed
  {
    return \apply_filters($tag, $value, ...$args);
  }
  public function addAction(string $tag, callable $function_to_add, int $priority = 10, int $accepted_args = 1): void
  {
    \add_action($tag, $function_to_add, $priority, $accepted_args);
  }
  public function removeAction(string $tag, callable $function_to_remove, int $priority = 10): bool
  {
    return \remove_action($tag, $function_to_remove, $priority);
  }
  public function doAction(string $tag, mixed ...$args): void
  {
    \do_action($tag, ...$args);
  }
}

แม้ว่าแนวคิดของ hooks จะมาจาก WordPress แต่ก็สามารถทำงานกับ CMS อื่นๆ ได้เช่นกัน (ตัวอย่างเช่น การใช้ events and listeners เพื่อ implement hooks) ดังนั้น เราสามารถแทนที่ hooks-wp ด้วย hooks-laravel, hooks-symfony, hooks-drupal, hooks-octobercms หรืออื่นๆ เพื่อปฏิบัติตาม contracts โดยใช้โค้ดที่เฉพาะเจาะจงกับแต่ละ CMS

อนุญาตให้ CMS ละทิ้งฟังก์ชันที่ไม่รองรับได้

ไม่ใช่ทุก CMS ที่รองรับฟังก์ชันทั้งหมด ตัวอย่างเช่น WordPress ช่วยให้สามารถ เรียงโพสต์ตาม meta_value entry ได้ แต่ OctoberCMS ทำไม่ได้

นั่นคือเหตุผลที่ GraphQL by PoP มีแพ็กเกจ metaquery (ซึ่งสำหรับ WordPress ปฏิบัติตามผ่าน metaquery-wp) ดังนั้น GraphQL server ที่ implement สำหรับ WordPress จะมีแพ็กเกจนี้ แต่ GraphQL server สำหรับ OctoberCMS จะไม่มี

ข้อดีของแนวทางนี้

การแบ่งแพ็กเกจอย่างละเอียดมีข้อได้เปรียบหลายประการ

แยก business logic ออกจากโค้ด CMS-specific

แทนที่จะเขียนโค้ดแอปพลิเคชันตามความเฉพาะของ CMS (วิธีการเขียนโค้ด ฟีเจอร์ ข้อจำกัด และอื่นๆ) เราสามารถทำให้โค้ดเป็นนามธรรมและใช้เฉพาะ business logic เท่านั้น

ตัวอย่างเช่น เพื่อรับรายการโพสต์ แอปพลิเคชันสามารถ execute เมธอด getPosts จาก interface บนแพ็กเกจ CMS-agnostic posts จากนั้นโพสต์จะถูกดึงข้อมูลในแบบเดิมเสมอ โดยไม่ขึ้นกับการ implement ของ CMS เบื้องหลัง

หลีกเลี่ยง technical debt และใช้มาตรฐานล่าสุด

ตามตัวอย่างข้างต้น เราดึงข้อมูลโพสต์ด้วยการ execute เมธอด getPosts ซึ่งเป็นไปตาม convention PSR-4 แทนที่จะเรียก get_posts ตามที่ WordPress กำหนด

ในทำนองเดียวกัน เราสามารถ execute getCustomPost เพื่อดึง custom post แทน get_post ที่ไม่ถูกต้อง (ซึ่งเป็นส่วนหนึ่งของ technical debt ของ WordPress)

ทำ scope ได้ง่าย

การใช้ PHP-Scoper เพื่อ scope WordPress plugin ไม่ใช่เรื่องง่าย และแม้จะทำได้ก็มีแนวโน้มที่จะเกิด bug

การรักษาโค้ด CMS-specific และ business logic ของแอปพลิเคชันให้แยกออกจากกันอย่างสมบูรณ์ ช่วยให้สามารถใช้ PHP-Scoper กับแพ็กเกจชุดหนึ่งเท่านั้น (ชุดที่มี business logic) และหลีกเลี่ยงในแพ็กเกจอื่นๆ (ชุดที่มีโค้ด WordPress) ผมได้อธิบายกลยุทธ์นี้อย่างละเอียด ที่นี่

นอกจากนี้ คล้ายกับ PHP-Scoper อาจมีเครื่องมืออื่นที่ล้มเหลวเมื่อนำไปใช้กับโค้ด CMS-specific บางอย่าง (เช่น WordPress) ในกรณีเหล่านั้น การแบ่งแพ็กเกจอย่างละเอียดสามารถช่วยแก้ปัญหาได้

สามารถสร้างแอปพลิเคชันที่แตกต่างกันซึ่งมีเฉพาะโค้ดที่ต้องการ

เราสามารถนำแพ็กเกจกลับมาใช้ใหม่เพื่อสร้างแอปพลิเคชันเพิ่มเติม โดยมีเฉพาะแพ็กเกจที่ต้องการและไม่มีอย่างอื่น

ตัวอย่างเช่น บล็อกส่วนตัวอาจต้องการเพียง posts, tags และ categories เท่านั้น จึงสามารถหลีกเลี่ยงการจัดการกับฟังก์ชัน users หรือ user-login

แท้จริงแล้ว ผมวางแผนที่จะใช้ประโยชน์จากฟีเจอร์นี้ในเร็วๆ นี้: ปัจจุบันผมกำลังทำงานกับ "Private GraphQL API" ซึ่งเป็น GraphQL engine แบบ self-contained เพื่อให้นักพัฒนา WordPress plugin สามารถ bundle ไว้ภายใน plugin ของตน ซึ่งให้ GraphQL API สำหรับ Gutenberg blocks ของพวกเขา

ผมสามารถสร้าง "Private GraphQL API" ได้อย่างง่ายดายเพียงแค่ลบแพ็กเกจที่ไม่จำเป็นออกจาก Gato GraphQL plugin (ที่จัดการกับ UI, clients, custom endpoints, HTTP caching, persisted queries และอื่นๆ)

สุดท้าย เนื่องจากทำ scope ได้ง่าย (ดังที่เห็นข้างต้น) ผมสามารถเพิ่ม prefix ให้กับแพ็กเกจที่จำเป็นทั้งหมด ดังนั้น Private GraphQL API จะทำงานได้โดยไม่มีความขัดแย้ง (ซึ่งอาจเกิดขึ้นเมื่อ plugin สองตัวที่แตกต่างกัน bundle Private GraphQL API เวอร์ชันต่างกัน)

ข้อเสียของแนวทางนี้

ไม่จำเป็นต้องพูดว่า แนวทางนี้ยังห่างไกลจากความสมบูรณ์แบบ

ต้องใช้ความพยายามมากขึ้น โค้ดยาวขึ้น

โดยปกติ ถ้าแอปพลิเคชันของเรารันบน WordPress เพื่อดึงรายการโพสต์เราแค่ execute get_posts เรียบง่ายและง่ายดาย

การทำให้เป็น CMS-agnostic ทำให้เรื่องซับซ้อนขึ้นอย่างมาก เพื่อดึงรายการโพสต์ เราต้อง:

  • สร้างแพ็กเกจ posts และ posts-wp
  • สร้าง contract ที่มีฟังก์ชัน getPosts ในแพ็กเกจ posts
  • ปฏิบัติตาม contract ผ่าน get_posts ในแพ็กเกจ posts-wp
  • ตรวจสอบให้แน่ใจเสมอว่าจะ invoke ฟังก์ชันผ่าน contract เสมอ ไม่ใช่โดยตรง

(น่าจะ) ต้องการ dependency injection

เราต้องผูก contract ทุกอย่างจากแพ็กเกจ CMS-agnostic และการ implement จากแพ็กเกจ CMS-specific ในกรณีของผม ผมใช้ service container ที่ Symfony's DependencyInjection component มอบให้

ผมชอบแนวทางนี้มาก ผมเชื่อว่ามันช่วยให้แอปพลิเคชันง่ายขึ้นอย่างมาก อย่างไรก็ตาม ผมเข้าใจว่าไม่ใช่ทุกแอปพลิเคชันที่จะต้องการ dependency injection ซึ่งเพิ่มความซับซ้อน

(น่าจะ) ต้องการ monorepo

Gato GraphQL ในที่สุดมี 91 แพ็กเกจ ในอดีต ผมโฮสต์แต่ละแพ็กเกจใน repository ของตัวเอง ทำให้การสร้าง PR เป็นเรื่องยากมาก จึง "ถูกบังคับ" ให้เปลี่ยนมาใช้แนวทาง monorepo

เพื่อให้ชัดเจน: ผมชอบ monorepo มาก แต่ผมเข้าใจว่าไม่ใช่ทุกคนที่ชอบ และมันก็ต้องการความพยายามในการดูแลรักษาของตัวเองเช่นกัน

ลิงก์ที่เป็นประโยชน์

ผมเคยเขียนเกี่ยวกับแรงจูงใจและกลยุทธ์ในการทำให้เว็บไซต์ WordPress ของผมเป็น abstract และ CMS-agnostic มาก่อน กลยุทธ์เดียวกันนี้เองที่ผมนำมาใช้เพื่อแบ่งโค้ดเบสสำหรับ Gato GraphQL:

ภาคผนวก: รายการ 91 แพ็กเกจที่ประกอบเป็นปลั๊กอิน

Gato GraphQL มี 91 แพ็กเกจดังต่อไปนี้

ฟังก์ชัน Engine:

getpop/access-control
getpop/cache-control
getpop/component-model
getpop/definitions
getpop/engine
getpop/engine-wp
getpop/field-query
getpop/guzzle-helpers
getpop/hooks
getpop/hooks-wp
getpop/loosecontracts
getpop/mandatory-directives-by-configuration
getpop/modulerouting
getpop/query-parsing
getpop/root
getpop/routing
getpop/routing-wp
getpop/translation
getpop/translation-wp
graphql-api/markdown-convertor

ฟังก์ชัน API:

getpop/api
getpop/api-clients
getpop/api-endpoints
getpop/api-endpoints-for-wp
getpop/api-graphql
getpop/api-mirrorquery

ฟังก์ชัน GraphQL server:

graphql-by-pop/graphql-clients-for-wp
graphql-by-pop/graphql-endpoint-for-wp
graphql-by-pop/graphql-parser
graphql-by-pop/graphql-query
graphql-by-pop/graphql-request
graphql-by-pop/graphql-server

Data model:

pop-schema/basic-directives
pop-schema/categories
pop-schema/categories-wp
pop-schema/comment-mutations
pop-schema/comment-mutations-wp
pop-schema/commentmeta
pop-schema/commentmeta-wp
pop-schema/comments
pop-schema/comments-wp
pop-schema/custompost-mutations
pop-schema/custompost-mutations-wp
pop-schema/custompostmedia
pop-schema/custompostmedia-mutations
pop-schema/custompostmedia-mutations-wp
pop-schema/custompostmedia-wp
pop-schema/custompostmeta
pop-schema/custompostmeta-wp
pop-schema/customposts
pop-schema/customposts-wp
pop-schema/generic-customposts
pop-schema/media
pop-schema/media-wp
pop-schema/menus
pop-schema/menus-wp
pop-schema/meta
pop-schema/metaquery
pop-schema/metaquery-wp
pop-schema/pages
pop-schema/pages-wp
pop-schema/post-categories
pop-schema/post-categories-wp
pop-schema/post-mutations
pop-schema/post-tags
pop-schema/post-tags-wp
pop-schema/posts
pop-schema/posts-wp
pop-schema/queriedobject
pop-schema/queriedobject-wp
pop-schema/schema-commons
pop-schema/tags
pop-schema/tags-wp
pop-schema/taxonomies
pop-schema/taxonomies-wp
pop-schema/taxonomymeta
pop-schema/taxonomymeta-wp
pop-schema/taxonomyquery
pop-schema/taxonomyquery-wp
pop-schema/user-roles
pop-schema/user-roles-access-control
pop-schema/user-roles-wp
pop-schema/user-state
pop-schema/user-state-access-control
pop-schema/user-state-mutations
pop-schema/user-state-mutations-wp
pop-schema/user-state-wp
pop-schema/usermeta
pop-schema/usermeta-wp
pop-schema/users
pop-schema/users-wp

สมัครรับจดหมายข่าวของเรา

ติดตามการอัปเดตทั้งหมดของ Gato GraphQL