๐๐ฝโโ๏ธ Mengapa untuk mendukung CMS-agnosticism, Gato GraphQL dipecah menjadi ~90 paket, serta keuntungan dan kekurangan dari pendekatan ini
Minggu lalu saya menerbitkan artikel ๐๐ปโโ๏ธ Mengapa Gato GraphQL membutuhkan Monorepo, dan bagaimana ia dioptimalkan, yang menjelaskan bagaimana dan mengapa monorepo GatoGraphQL/GatoGraphQL, yang menampung kode untuk Gato GraphQL, dapat mengelola codebase plugin secara efisien.
Saya membagikan artikel saya di Reddit, dan mendapatkan komentar berikut:
Artikel dari OP dan artikel-artikel yang ditautkannya, terasa seperti monorepo adalah hal terbaik yang pernah ada.
Artikel yang lebih menarik adalah menjelaskan mengapa Anda berpikir CMS-agnosticism mengharuskan pemecahan segalanya menjadi paket-paket kecil tersendiri, dan mengapa Anda berpikir setiap dari lebih dari 200 paket perlu berada di repo tersendiri sejak awal.
Ini adalah pertanyaan yang menarik. Jadi saya memutuskan untuk menulis artikel ini, untuk membahasnya lebih lanjut.
Namun pertama-tama, saya akan membahas dua topik terkait: berapa banyak paket yang sebenarnya dibutuhkan oleh plugin, dan mengapa saya mengklaim bahwa server GraphQL yang mendasarinya bersifat CMS-agnostic.
Berapa banyak paket yang menyusun plugin
Meskipun saya telah menyebut lebih dari 200 paket PHP, itu untuk monorepo; untuk plugin, jumlahnya sebenarnya jauh lebih sedikit dari itu.
Monorepo GatoGraphQL/GatoGraphQL mencakup 5 proyek:
- PoP, sebuah library model komponen sisi server (seperti React, tetapi untuk back-end)
- GraphQL by PoP, sebuah server GraphQL CMS-agnostic untuk PHP
- Gato GraphQL
- sebuah site builder (WIP)
- Wassup, sebuah tema website berbasis site builder (WIP)
Menghosting proyek-proyek ini dalam satu monorepo menyederhanakan pekerjaan dengan mereka, karena adanya ketergantungan satu sama lain:
- GraphQL by PoP berbasis pada PoP
- Gato GraphQL berbasis pada GraphQL by PoP
- Site builder menggunakan library model komponen sebagai mesinnya (mirip dengan Gatsby yang menggunakan GraphQL)
- Wassup berbasis pada site builder
Adalah mengenai kode untuk semua 5 proyek itulah GatoGraphQL/GatoGraphQL berisi lebih dari 200 paket PHP. Mengenai Gato GraphQL, jumlahnya "hanya" 91 paket. Dan GraphQL by PoP, server GraphQL yang mendasarinya, berisi "hanya" 98 paket.
(Plugin Gato GraphQL membutuhkan lebih sedikit paket daripada server GraphQL yang mendasarinya, karena beberapa paket, seperti direktif @strTranslate Google Translate, belum ditambahkan ke plugin.)
Bagaimana GraphQL by PoP bisa CMS-agnostic? Apa bedanya dengan webonyx?
Saya telah mengatakan bahwa GraphQL by PoP bersifat CMS-agnostic. Tetapi apa artinya itu?
Dalam hal ini, webonyx/graphql-php juga bersifat CMS-agnostic. Lalu apa perbedaannya?
webonyx/graphql-php bersifat CMS-agnostic, dalam artian ia adalah sebuah paket yang didistribusikan melalui Composer, yang hanya berisi kode PHP "vanilla". Namun, ia bukanlah server GraphQL yang berdiri sendiri; melainkan, ia adalah implementasi PHP dari spesifikasi GraphQL, yang akan ditanamkan di dalam suatu server GraphQL dalam PHP.
Kini, server GraphQL yang mengimplementasikannya, seperti Lighthouse atau WPGraphQL, tidaklah bersifat CMS-agnostic. Kita tidak bisa menjalankan Lighthouse di WordPress, atau WPGraphQL di Laravel.
Inilah pengertian bahwa GraphQL by PoP bersifat CMS-agnostic: ia adalah server GraphQL yang "hampir-final", hampir siap dijalankan dengan CMS atau framework apapun, baik Laravel, WordPress, maupun yang lainnya. (Demi kesingkatan, mulai sekarang, setiap kali saya menyebut "CMS", artinya "CMS atau framework".)
Untuk membuatnya final untuk suatu CMS, server GraphQL masih memerlukan beberapa kode kustom untuk CMS tersebut, melalui beberapa paket yang sesuai.
Saya sekarang akan membahas pertanyaan-pertanyaan dalam komentar tersebut.
Mengapa setiap paket perlu berada di repo tersendiri
Karena Packagist (registry paket PHP milik Composer) mengharuskan penyediaan URL repositori untuk mempublikasikan/mendistribusikan sebuah paket.
(Omong-omong, artikel saya Hosting all your PHP packages together in a monorepo, yang juga diterbitkan minggu lalu, membahas masalah ini.)
Mengapa CMS-agnosticism mengharuskan pemecahan segalanya menjadi paket-paket kecil tersendiri
Ada beberapa alasan.
Membiarkan CMS menyuntikkan kodenya sendiri
Mustahil untuk membuat server GraphQL yang bekerja di mana saja, menggunakan 100% kode PHP yang sama.
Misalnya, untuk memungkinkan bagian kode mana pun memodifikasi nilai suatu variabel di tempat lain, WordPress bergantung pada filter hooks, Symfony menggunakan komponen EventDispatcher, dan Laravel memiliki sistemnya sendiri berupa events dan listeners. Kode PHP untuk 3 metode yang berbeda ini juga akan berbeda.
Di sinilah pendekatan pemecahan kode menjadi paket-paket granular berperan. Alih-alih menempatkan solusi untuk events dan listeners sebagai bagian dari aplikasi, ia disuntikkan ke dalam aplikasi melalui sebuah paket, dan paket ini akan berisi kode yang spesifik untuk CMS tersebut.
Agar ini berfungsi, setiap fungsionalitas harus dipecah menjadi 2 paket:
- sebuah paket CMS-agnostic, yang berisi semua logika bisnis, hanya menggunakan kode PHP "vanilla". Paket ini akan menyertakan kontrak-kontrak yang harus dipenuhi oleh paket spesifik CMS
- sebuah paket spesifik CMS, yang memenuhi kontrak-kontrak untuk CMS tersebut
Misalnya, GraphQL by PoP memiliki paket hooks yang berisi kontrak berikut:
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;
}Dan kemudian, paket hooks-wp memenuhi kontrak untuk 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);
}
}Kini, meskipun konsep hooks berasal dari WordPress, ia dapat bekerja dengan CMS lain juga (misalnya, menggunakan events dan listeners untuk mengimplementasikan hooks). Kemudian, kita dapat mengganti hooks-wp dengan hooks-laravel, hooks-symfony, hooks-drupal, hooks-octobercms, atau yang lainnya, untuk memenuhi kontrak menggunakan kode yang spesifik untuk setiap CMS.
Mengizinkan CMS membuang fungsionalitas yang tidak didukungnya
Tidak semua CMS dapat mendukung semua fungsionalitas. Misalnya, WordPress memungkinkan untuk mengurutkan posting berdasarkan entri meta_value tertentu, tetapi OctoberCMS tidak.
Itulah mengapa GraphQL by PoP berisi paket metaquery (dipenuhi untuk WordPress melalui metaquery-wp). Kemudian, server GraphQL yang diimplementasikan untuk WordPress akan menyertakan paket ini, tetapi yang untuk OctoberCMS tidak.
Keuntungan dari pendekatan ini
Memecah paket-paket secara granular menawarkan beberapa keunggulan.
Memisahkan logika bisnis dari kode spesifik CMS
Alih-alih mengkode aplikasi berdasarkan opinionatedness (cara pengkodean, fitur, keterbatasan, dan lainnya) dari suatu CMS, kita dapat mengabstraksikan kode kita dan hanya menggunakan logika bisnis.
Misalnya, untuk mendapatkan daftar posting, aplikasi dapat mengeksekusi metode getPosts dari suatu antarmuka pada paket CMS-agnostic posts. Kemudian, posting akan selalu diambil dengan cara yang sama, terlepas dari implementasi oleh CMS yang mendasarinya.
Melewati technical debt, dan menggunakan standar terbaru
Mengikuti contoh di atas, kita mengambil posting kita dengan mengeksekusi metode getPosts, yang mengikuti konvensi PSR-4, alih-alih memanggil get_posts, sebagaimana didefinisikan oleh WordPress.
Demikian pula, kita dapat mengeksekusi getCustomPost untuk mengambil custom post, alih-alih menggunakan get_post yang tidak akurat (ini adalah bagian dari technical debt WordPress).
Mudah untuk di-scope
Menggunakan PHP-Scoper untuk men-scope plugin WordPress tidaklah mudah, dan bahkan ketika bisa dilakukan, hal itu rentan terhadap bug.
Menjaga kode spesifik CMS dan logika bisnis aplikasi tetap terpisah secara menyeluruh, memungkinkan penerapan PHP-Scoper hanya pada satu set paket (yang berisi logika bisnis), dan menghindarinya pada yang lainnya (yang berisi kode WordPress). Saya telah mendeskripsikan strategi ini secara rinci, di sini.
Selain itu, mirip dengan PHP-Scoper, mungkin ada alat-alat lain yang gagal saat diterapkan pada beberapa kode spesifik CMS (seperti WordPress). Dalam kasus-kasus tersebut, memecah paket secara granular dapat menyelamatkan situasi.
Kita dapat menghasilkan aplikasi-aplikasi berbeda, masing-masing hanya berisi kode yang dibutuhkannya
Kita dapat menggunakan kembali paket-paket kita untuk menghasilkan lebih banyak aplikasi, yang hanya berisi paket-paket yang dibutuhkannya dan tidak lebih.
Misalnya, sebuah blog pribadi mungkin hanya membutuhkan posts, tags dan categories, sehingga ia dapat menghindari urusan dengan fungsionalitas untuk users atau user-login.
Memang, saya berencana untuk memanfaatkan fitur ini segera: saat ini saya sedang mengerjakan "Private GraphQL API", sebuah mesin GraphQL yang berdiri sendiri, yang akan tersedia bagi para pengembang plugin WordPress untuk dibundel di dalam plugin mereka, sehingga memberikan GraphQL API untuk blok Gutenberg mereka.
Saya dapat dengan mudah membuat "Private GraphQL API" hanya dengan menghapus paket-paket dari plugin Gato GraphQL yang tidak diperlukan (yang berhubungan dengan UI, clients, custom endpoints, HTTP caching, persisted queries, dan beberapa lainnya).
Terakhir, karena mudah untuk di-scope (seperti yang dilihat di atas), saya dapat menambahkan prefix pada semua paket yang diperlukan, sehingga Private GraphQL API akan bekerja tanpa konflik (yang bisa terjadi ketika 2 plugin berbeda membundel versi yang berbeda dari Private GraphQL API).
Kekurangan dari pendekatan ini
Tak perlu dikatakan, pendekatan ini masih jauh dari sempurna.
Upaya lebih besar, kode menjadi lebih verbose
Biasanya, jika aplikasi kita berjalan di WordPress, untuk mengambil daftar posting kita cukup mengeksekusi get_posts. Sederhana dan mudah.
Membuatnya CMS-agnostic memperumit segalanya secara signifikan. Untuk mengambil daftar posting, kita harus:
- Membuat paket
postsdanposts-wp - Membuat kontrak dengan fungsi
getPostsdi paketposts - Memenuhi kontrak melalui
get_postsdi paketposts-wp - Selalu memastikan untuk memanggil fungsionalitas melalui kontrak, tidak pernah secara langsung
Ini (hampir pasti) memerlukan dependency injection
Kita perlu mengikat setiap kontrak dari paket CMS-agnostic, dan implementasinya dari paket spesifik CMS. Dalam kasus saya, saya menggunakan service container, yang disediakan oleh komponen DependencyInjection Symfony.
Saya menyukai pendekatan ini, saya percaya ini sangat menyederhanakan aplikasi. Namun, saya mengerti bahwa tidak setiap aplikasi akan memerlukan dependency injection, yang menambahkan kompleksitas padanya.
Ini (kemungkinan besar) memerlukan monorepo
Gato GraphQL akhirnya berisi 91 paket. Di masa lalu, saya menghosting masing-masingnya di repositorinya sendiri, yang membuat pembuatan PR menjadi sangat sulit. Jadi saya "dipaksa" untuk beralih ke pendekatan monorepo.
Untuk lebih jelas: saya benar-benar menyukai monorepo. Namun saya mengerti bahwa tidak semua orang menyukainya, dan itu juga memerlukan usaha tersendiri untuk dikelola.
Tautan berguna
Saya sebelumnya telah menulis tentang motivasi dan strategi saya untuk mengabstraksikan website WordPress saya, membuatnya CMS-agnostic. Inilah strategi yang sama yang saya terapkan untuk memecah codebase Gato GraphQL:
- Abstracting WordPress Code To Reuse With Other CMSs: Concepts (Part 1)
- Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)
Addendum: Daftar 91 paket yang menyusun plugin
Gato GraphQL berisi 91 paket berikut.
Fungsionalitas mesin:
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
Fungsionalitas API:
getpop/api
getpop/api-clients
getpop/api-endpoints
getpop/api-endpoints-for-wp
getpop/api-graphql
getpop/api-mirrorquery
Fungsionalitas 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
Model data:
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