Blog

💁🏽‍♂️ Tại sao để hỗ trợ CMS-agnosticism, Gato GraphQL được chia thành ~90 packages, và ưu nhược điểm của cách tiếp cận này

Leonardo Losoviz
Bởi Leonardo Losoviz ·

Tuần trước tôi đã đăng bài viết 💁🏻‍♀️ Tại sao Gato GraphQL cần một Monorepo, và cách nó được tối ưu hóa, giải thích cách thức và lý do tại sao monorepo GatoGraphQL/GatoGraphQL, nơi chứa mã nguồn của Gato GraphQL, có thể quản lý codebase của plugin một cách hiệu quả.

Tôi đã chia sẻ bài viết trên Reddit và nhận được bình luận sau:

Bài viết của OP và các bài viết mà nó liên kết đến đọc như thể monorepo là thứ tuyệt vời nhất kể từ khi có bánh mì cắt lát.

Một bài viết thú vị hơn sẽ là giải thích tại sao bạn nghĩ rằng CMS-agnosticism đòi hỏi phải chia nhỏ mọi thứ thành các package riêng lẻ, và tại sao bạn nghĩ rằng từng package trong số hơn 200 packages cần phải có repo riêng ngay từ đầu.

Đây là một câu hỏi thú vị. Vì vậy tôi quyết định viết bài viết này để giải đáp thêm một chút.

Nhưng trước tiên, tôi sẽ đề cập đến hai chủ đề liên quan: thực tế plugin cần bao nhiêu packages, và tại sao tôi cho rằng server GraphQL bên dưới là CMS-agnostic.

Plugin được cấu thành từ bao nhiêu packages

Mặc dù tôi đã đề cập đến hơn 200 packages PHP, nhưng con số đó là dành cho monorepo; đối với plugin, thực tế ít hơn nhiều.

Monorepo GatoGraphQL/GatoGraphQL bao gồm 5 dự án:

  1. PoP, một thư viện mô hình component phía server (giống như React, nhưng cho back-end)
  2. GraphQL by PoP, một server GraphQL CMS-agnostic cho PHP
  3. Gato GraphQL
  4. một trình xây dựng trang web (WIP)
  5. Wassup, một theme website dựa trên trình xây dựng trang web (WIP)

Việc lưu trữ các dự án này trong một monorepo giúp đơn giản hóa việc làm việc với chúng, vì các phụ thuộc lẫn nhau:

  • GraphQL by PoP được xây dựng dựa trên PoP
  • Gato GraphQL được xây dựng dựa trên GraphQL by PoP
  • Trình xây dựng trang web sử dụng thư viện mô hình component làm engine (tương tự Gatsby sử dụng GraphQL)
  • Wassup được xây dựng dựa trên trình xây dựng trang web

Đó là về mã nguồn của cả 5 dự án mà GatoGraphQL/GatoGraphQL chứa hơn 200 packages PHP. Riêng về Gato GraphQL, con số là "chỉ" 91 packages. Và GraphQL by PoP, server GraphQL bên dưới, chứa "chỉ" 98 packages.

(Plugin Gato GraphQL yêu cầu ít packages hơn server GraphQL bên dưới của nó, vì một số packages, chẳng hạn như directive @strTranslate của Google Translate, chưa được thêm vào plugin.)

GraphQL by PoP là CMS-agnostic theo nghĩa nào? Nó khác gì với webonyx?

Tôi đã nói rằng GraphQL by PoP là CMS-agnostic. Nhưng điều đó có nghĩa là gì?

Về vấn đề đó, webonyx/graphql-php cũng là CMS-agnostic. Vậy chúng khác nhau thế nào?

webonyx/graphql-php là CMS-agnostic, theo nghĩa nó là một package được phân phối qua Composer, chỉ chứa mã PHP "vanilla". Tuy nhiên, bản thân nó không phải là một server GraphQL hoàn chỉnh; thay vào đó, nó là một triển khai trong PHP của đặc tả GraphQL, được nhúng vào bên trong một server GraphQL nào đó bằng PHP.

Bây giờ, các server GraphQL triển khai đặc tả này, chẳng hạn như Lighthouse hay WPGraphQL, không phải là CMS-agnostic. Chúng ta không thể chạy Lighthouse trên WordPress, hay WPGraphQL trên Laravel.

Chính theo nghĩa này mà GraphQL by PoP là CMS-agnostic: nó là server GraphQL "gần như hoàn chỉnh", gần như sẵn sàng để chạy với bất kỳ CMS hay framework nào, dù là Laravel, WordPress, hay bất kỳ cái nào khác. (Để ngắn gọn, từ đây về sau, khi tôi nói "CMS", ý tôi là "CMS hoặc framework".)

Để hoàn thiện nó cho một CMS cụ thể, server GraphQL vẫn cần một số mã tùy chỉnh cho CMS đó, thông qua một package tương ứng.

Tôi sẽ giải đáp các câu hỏi trong bình luận ngay bây giờ.

Tại sao mỗi package cần phải có repo riêng

Bởi vì Packagist (registry của Composer dành cho các packages PHP) yêu cầu cung cấp URL repository để phát hành/phân phối một package.

(Nhân tiện, bài viết của tôi Hosting all your PHP packages together in a monorepo, cũng được đăng tuần trước, nói về vấn đề này.)

Tại sao CMS-agnosticism đòi hỏi phải chia nhỏ mọi thứ thành các package riêng lẻ

Có một vài lý do.

Để CMS có thể đưa vào mã của chính nó

Không thể tạo ra một server GraphQL hoạt động ở khắp nơi, sử dụng 100% cùng một mã PHP.

Ví dụ, để cho phép bất kỳ đoạn mã nào có thể thay đổi giá trị của một biến ở nơi khác, WordPress dựa vào filter hooks, Symfony sử dụng component EventDispatcher, và Laravel có hệ thống events và listeners riêng. Mã PHP cho 3 phương pháp khác nhau này cũng sẽ khác nhau.

Đây là lúc cách tiếp cận chia mã thành các packages chi tiết phát huy tác dụng. Thay vì để giải pháp cho events và listeners là một phần của ứng dụng, nó được đưa vào ứng dụng thông qua một package, và package này sẽ chứa mã đặc thù cho CMS.

Để điều này hoạt động, mọi chức năng phải được chia thành 2 packages:

  • một package CMS-agnostic, chứa toàn bộ logic nghiệp vụ, chỉ sử dụng mã PHP "vanilla". Package này sẽ bao gồm các contract cần được package đặc thù CMS đáp ứng
  • một package đặc thù CMS, đáp ứng các contract cho CMS đó

Ví dụ, GraphQL by PoP có package hooks chứa contract sau:

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;
}

Và sau đó, package hooks-wp đáp ứng contract cho 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);
  }
}

Bây giờ, mặc dù khái niệm hooks xuất phát từ WordPress, nó cũng có thể hoạt động với các CMS khác (ví dụ, sử dụng events và listeners để triển khai hooks). Sau đó, chúng ta có thể thay thế hooks-wp bằng hooks-laravel, hooks-symfony, hooks-drupal, hooks-octobercms, hoặc bất kỳ cái nào khác, để đáp ứng các contract bằng mã đặc thù cho từng CMS.

Cho phép CMS loại bỏ các chức năng mà nó không thể hỗ trợ

Không phải tất cả các CMS đều có thể hỗ trợ tất cả các chức năng. Ví dụ, WordPress cho phép sắp xếp bài viết theo một mục meta_value, nhưng OctoberCMS thì không.

Đó là lý do tại sao GraphQL by PoP có package metaquery (được đáp ứng cho WordPress thông qua metaquery-wp). Sau đó, server GraphQL được triển khai cho WordPress sẽ bao gồm package này, nhưng server dành cho OctoberCMS thì không.

Ưu điểm của cách tiếp cận này

Việc chia các packages theo kiểu chi tiết mang lại một số lợi thế.

Tách biệt logic nghiệp vụ khỏi mã đặc thù CMS

Thay vì lập trình ứng dụng dựa trên tính opinionated (cách lập trình, tính năng, hạn chế, và các yếu tố khác) của một CMS, chúng ta có thể trừu tượng hóa mã của mình và chỉ sử dụng logic nghiệp vụ.

Ví dụ, để lấy một danh sách bài viết, ứng dụng có thể thực thi phương thức getPosts từ một interface trong package CMS-agnostic posts. Sau đó, các bài viết sẽ luôn được lấy theo cùng một cách, bất kể việc triển khai của CMS bên dưới là gì.

Vượt qua nợ kỹ thuật và sử dụng các tiêu chuẩn mới nhất

Theo ví dụ trên, chúng ta lấy các bài viết bằng cách thực thi phương thức getPosts, tuân theo quy ước PSR-4, thay vì gọi get_posts như WordPress định nghĩa.

Tương tự, chúng ta có thể thực thi getCustomPost để lấy một custom post, thay vì dùng get_post không chính xác (đây là một phần nợ kỹ thuật của WordPress).

Dễ dàng scope

Sử dụng PHP-Scoper để scope một plugin WordPress không phải là dễ, và ngay cả khi có thể thực hiện được, nó vẫn dễ gây ra lỗi.

Việc giữ cho mã đặc thù CMS và logic nghiệp vụ của ứng dụng được tách biệt hoàn toàn cho phép áp dụng PHP-Scoper trên chỉ một tập hợp packages (những cái chứa logic nghiệp vụ), và tránh áp dụng trên các cái khác (những cái chứa mã WordPress). Tôi đã mô tả chiến lược này chi tiết, tại đây.

Ngoài ra, tương tự như PHP-Scoper, có thể có những công cụ khác thất bại khi áp dụng trên mã đặc thù CMS (chẳng hạn như WordPress). Trong những trường hợp đó, việc chia các packages theo kiểu chi tiết có thể là giải pháp cứu cánh.

Chúng ta có thể tạo ra các ứng dụng khác nhau, mỗi cái chỉ chứa mã mà nó cần

Chúng ta có thể tái sử dụng các packages của mình để tạo ra nhiều ứng dụng hơn, mỗi ứng dụng chỉ chứa những packages mà nó cần và không gì khác.

Ví dụ, một blog cá nhân có thể chỉ cần posts, tagscategories, vì vậy nó có thể tránh phải xử lý chức năng cho users hay user-login.

Thật vậy, tôi dự định sẽ sớm tận dụng tính năng này: hiện tôi đang làm việc trên "Private GraphQL API", một engine GraphQL độc lập, sẽ được cung cấp cho các nhà phát triển plugin WordPress để tích hợp vào các plugin của họ, cấp một GraphQL API cho các block Gutenberg của họ.

Tôi có thể dễ dàng tạo ra "Private GraphQL API" chỉ đơn giản bằng cách loại bỏ những packages từ plugin Gato GraphQL mà không cần thiết (những cái liên quan đến UI, clients, custom endpoints, HTTP caching, persisted queries, và một vài cái khác).

Cuối cùng, vì dễ dàng scope (như đã thấy ở trên), tôi có thể thêm prefix vào tất cả các packages cần thiết, để Private GraphQL API hoạt động mà không có xung đột (điều có thể xảy ra khi 2 plugin khác nhau tích hợp các phiên bản khác nhau của Private GraphQL API).

Nhược điểm của cách tiếp cận này

Không cần phải nói, cách tiếp cận này còn xa mới hoàn hảo.

Tốn nhiều công sức hơn, mã trở nên dài dòng hơn

Thông thường, nếu ứng dụng của chúng ta chạy trên WordPress, để lấy danh sách bài viết, chúng ta chỉ cần thực thi get_posts. Đơn giản và dễ dàng.

Việc làm cho nó CMS-agnostic khiến mọi thứ phức tạp hơn đáng kể. Để lấy danh sách bài viết, chúng ta phải:

  • Tạo các packages postsposts-wp
  • Tạo một contract với hàm getPosts trong package posts
  • Đáp ứng contract thông qua get_posts trong package posts-wp
  • Luôn đảm bảo gọi chức năng thông qua contract, không bao giờ gọi trực tiếp

(Rất có khả năng) đòi hỏi dependency injection

Chúng ta cần liên kết mọi contract từ package CMS-agnostic, và triển khai của nó từ package đặc thù CMS. Trong trường hợp của tôi, tôi đang sử dụng một service container, được cung cấp bởi component DependencyInjection của Symfony.

Tôi yêu thích cách tiếp cận này, tôi tin rằng nó giúp đơn giản hóa ứng dụng rất nhiều. Tuy nhiên, tôi hiểu rằng không phải mọi ứng dụng đều cần đến dependency injection, điều này thêm vào độ phức tạp của nó.

(Rất có khả năng) đòi hỏi một monorepo

Gato GraphQL cuối cùng chứa đến 91 packages. Trước đây, tôi lưu trữ từng cái trong repo riêng của nó, khiến việc tạo PR rất khó khăn. Vì vậy tôi đã "bị buộc" phải chuyển sang cách tiếp cận monorepo.

Để rõ ràng: tôi thực sự thích monorepo. Nhưng tôi hiểu rằng không phải ai cũng thích nó, và nó cũng đòi hỏi nỗ lực bảo trì riêng.

Các liên kết hữu ích

Trước đây tôi đã viết về động lực và chiến lược của mình khi trừu tượng hóa website WordPress, làm cho nó CMS-agnostic. Đây chính là chiến lược mà tôi đã áp dụng để chia nhỏ codebase của Gato GraphQL:

Phụ lục: Danh sách 91 packages cấu thành plugin

Gato GraphQL bao gồm 91 packages sau đây.

Chức năng 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

Chức năng API:

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

Chức năng server GraphQL:

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

Mô hình dữ liệu:

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

Đăng ký nhận bản tin của chúng tôi

Cập nhật tất cả những điều mới từ Gato GraphQL.