Blog

💁🏽‍♂️ CMS-agnosticism'i desteklemek için Gato GraphQL neden ~90 pakete bölündü ve bu yaklaşımın avantajları ile dezavantajları

Leonardo Losoviz
Yazan: Leonardo Losoviz ·

Geçen hafta 💁🏻‍♀️ Gato GraphQL neden bir Monorepo'ya ihtiyaç duyar ve nasıl optimize edilmiştir başlıklı makaleyi yayımladım; bu makalede Gato GraphQL için kod barındıran GatoGraphQL/GatoGraphQL monorepo'sunun plugin'in kod tabanını nasıl verimli şekilde yönettiğini açıkladım.

Makalemi Reddit'te paylaştım ve şu yorumu aldım:

OP'nin makalesi ve bağlantı verdiği makaleler, sanki monorepo dilimli ekmekten bu yana icat edilmiş en harika şeymiş gibi okunuyor.

Daha ilginç bir makale, CMS-agnosticism'in her şeyi kendi küçük paketine bölmeyi neden gerektirdiğini ve 200'den fazla paketin her birinin neden başından beri kendi deposunda olması gerektiğini düşündüğünüzü açıklayan bir makale olurdu.

Bu ilginç bir soru. Bu yüzden konuyu biraz daha derinlemesine ele almak için bu makaleyi yazmaya karar verdim.

Ancak önce iki ilgili konuya değineceğim: plugin'in aslında kaç pakete ihtiyaç duyduğu ve temel GraphQL sunucusunun neden CMS-agnostic olduğunu iddia ettiğim.

Plugin kaç paketten oluşuyor

200'den fazla PHP paketinden bahsetmiş olsam da bu monorepo içindir; plugin için bu sayı aslında çok daha azdır.

GatoGraphQL/GatoGraphQL monorepo'su 5 projeyi kapsar:

  1. PoP, sunucu taraflı bir bileşen model kütüphanesi (React gibi ama back-end için)
  2. GraphQL by PoP, PHP için CMS-agnostic bir GraphQL sunucusu
  3. Gato GraphQL
  4. bir site oluşturucu (WIP)
  5. Wassup, site oluşturucuya dayalı bir web sitesi teması (WIP)

Bu projeleri bir monorepo'da barındırmak, aralarındaki bağımlılıklar nedeniyle onlarla çalışmayı kolaylaştırır:

  • GraphQL by PoP, PoP'a dayanır
  • Gato GraphQL, GraphQL by PoP'a dayanır
  • Site oluşturucu, bileşen model kütüphanesini motor olarak kullanır (Gatsby'nin GraphQL kullanmasına benzer şekilde)
  • Wassup, site oluşturucuya dayanır

GatoGraphQL/GatoGraphQL'in 200'den fazla PHP paketi içermesi tüm 5 projenin koduna ilişkindir. Gato GraphQL özelinde ise "yalnızca" 91 pakettir. Ve temel GraphQL sunucusu olan GraphQL by PoP "yalnızca" 98 paket içerir.

(Gato GraphQL plugin'i, temel GraphQL sunucusundan daha az pakete ihtiyaç duyar; çünkü Google Translate @strTranslate direktifi gibi bazı paketler henüz plugin'e eklenmemiştir.)

GraphQL by PoP nasıl CMS-agnostic? webonyx'ten farkı nedir?

GraphQL by PoP'un CMS-agnostic olduğunu söylüyorum. Peki bu ne anlama geliyor?

Nitekim webonyx/graphql-php da CMS-agnostic'tir. Peki aralarındaki fark nedir?

webonyx/graphql-php CMS-agnostic'tir; yani Composer aracılığıyla dağıtılan, yalnızca "vanilla" PHP kodu içeren bir pakettir. Ancak kendi başına tam bir GraphQL sunucusu değildir; bunun yerine PHP'de bir GraphQL sunucusuna gömülmek üzere GraphQL spesifikasyonunun PHP'deki bir uygulamasıdır.

Şimdi, Lighthouse veya WPGraphQL gibi bu GraphQL sunucularını uygulayan sistemler CMS-agnostic değildir. Lighthouse'u WordPress üzerinde ya da WPGraphQL'i Laravel üzerinde çalıştıramayız.

GraphQL by PoP'un CMS-agnostic olması tam da bu anlamda: "neredeyse nihai" GraphQL sunucusu olup Laravel, WordPress veya herhangi bir CMS ya da framework ile çalışmaya neredeyse hazırdır. (Kısalık adına, bundan böyle "CMS" dediğimde "CMS veya framework" kastediyorum.)

Belirli bir CMS için nihai hale getirmek amacıyla, GraphQL sunucusu yine de ilgili paket aracılığıyla o CMS'e özgü biraz özel kod gerektirir.

Şimdi yorumdaki sorulara geçeyim.

Her paketin neden kendi deposunda olması gerekiyordu

Çünkü Packagist (Composer'ın PHP paket kayıt defteri), bir paketi yayımlamak/dağıtmak için bir depo URL'si sağlanmasını zorunlu kılar.

(Bu arada, geçen hafta da yayımlanan Hosting all your PHP packages together in a monorepo başlıklı makalem bu konuyu ele alıyor.)

CMS-agnosticism neden her şeyin kendi küçük paketine bölünmesini gerektiriyor

Birkaç nedeni var.

CMS'in kendi kodunu enjekte etmesine izin vermek

%100 aynı PHP kodunu kullanarak her yerde çalışan bir GraphQL sunucusu yapmak imkânsızdır.

Örneğin, herhangi bir kod parçasının başka bir yerdeki bir değişkenin değerini değiştirebilmesini sağlamak için WordPress filter hooks'a güvenirken, Symfony EventDispatcher bileşenini kullanır ve Laravel'in kendi events ve listeners sistemi vardır. Bu 3 farklı yöntemin PHP kodu da birbirinden farklıdır.

İşte burada kodu ayrıntılı paketlere bölme yaklaşımı devreye girer. Olaylar ve dinleyiciler için bir çözümün uygulamanın bir parçası olması yerine, bir paket aracılığıyla uygulamaya enjekte edilir ve bu paket CMS'e özgü kod içerecektir.

Bunun işe yaraması için her işlevselliğin 2 pakete bölünmesi gerekir:

  • Yalnızca "vanilla" PHP kodu kullanarak tüm iş mantığını içeren CMS-agnostic bir paket. Bu paket, CMS'e özgü paketin karşılaması gereken sözleşmeleri içerecektir
  • O CMS için sözleşmeleri karşılayan CMS'e özgü bir paket

Örneğin, GraphQL by PoP, şu sözleşmeyi içeren bir hooks paketine sahiptir:

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

Ardından hooks-wp paketi WordPress için sözleşmeyi karşılar:

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

Şimdi, hooks kavramı WordPress'ten gelse de diğer CMS'lerle de çalışabilir (örneğin, hooks'u uygulamak için events ve listeners kullanarak). Bu durumda sözleşmeleri her CMS'e özgü kodla karşılamak için hooks-wp yerine hooks-laravel, hooks-symfony, hooks-drupal, hooks-octobercms veya başka herhangi birini kullanabiliriz.

CMS'in destekleyemeyeceği işlevselliği devre dışı bırakmasına izin vermek

Her CMS her işlevselliği destekleyemez. Örneğin, WordPress gönderileri bir meta_value girişine göre sıralamayı mümkün kılarken OctoberCMS bunu desteklemez.

Bu nedenle GraphQL by PoP, metaquery paketini içerir (WordPress için metaquery-wp aracılığıyla karşılanır). Böylece WordPress için uygulanan GraphQL sunucusu bu paketi içerecek, OctoberCMS için olan ise içermeyecektir.

Bu yaklaşımın avantajları

Paketlerimizi ayrıntılı biçimde bölmek birkaç avantaj sunar.

İş mantığını CMS'e özgü koddan ayırmak

Uygulamayı bir CMS'in önyargılılığına (kodlama biçimi, özellikler, sınırlamalar ve diğerleri) göre kodlamak yerine kodumuzu soyutlayabilir ve yalnızca iş mantığını kullanabiliriz.

Örneğin, bir gönderi listesi almak için uygulama, CMS-agnostic posts paketindeki bir arayüzden getPosts metodunu çalıştırabilir. Bu durumda gönderiler, temel CMS'in uygulamasından bağımsız olarak her zaman aynı şekilde alınır.

Teknik borçtan kaçınmak ve en güncel standartları kullanmak

Yukarıdaki örneği takip ederek, gönderilerimizi WordPress tarafından tanımlanan get_posts'u çağırmak yerine PSR-4 kuralını izleyen getPosts metodunu çalıştırarak alırız.

Benzer şekilde, hatalı get_post yerine (bu WordPress'in teknik borcunun bir parçasıdır) özel bir gönderiyi almak için getCustomPost'u çalıştırabiliriz.

Kapsamlandırmak kolaydır

Bir WordPress plugin'ini kapsamlandırmak için PHP-Scoper kullanmak kolay değildir ve yapılabilir olduğunda bile hatalara yatkındır.

CMS'e özgü kodu ve uygulamanın iş mantığını tamamen ayrık tutmak, PHP-Scoper'ı yalnızca bir paket kümesine (iş mantığını içerenler) uygulamayı ve diğerlerinden (WordPress kodu içerenler) kaçınmayı mümkün kılar. Bu stratejiyi ayrıntılı biçimde burada açıkladım.

Bunun yanı sıra, PHP-Scoper'a benzer şekilde, bazı CMS'e özgü kodlara (WordPress gibi) uygulandığında başarısız olan başka araçlar da olabilir. Bu durumlarda paketleri ayrıntılı biçimde bölmek işe yarayabilir.

Yalnızca ihtiyaç duyduğu kodu içeren farklı uygulamalar üretebiliriz

Paketlerimizi yeniden kullanarak yalnızca ihtiyaç duydukları paketleri içeren ve başka hiçbir şey içermeyen daha fazla uygulama üretebiliriz.

Örneğin, kişisel bir blog yalnızca posts, tags ve categories'e ihtiyaç duyabilir; böylece users veya user-login işlevselliğiyle uğraşmak zorunda kalmaz.

Nitekim bu özellikten yakında yararlanmayı planlıyorum: şu anda kendi kendine yeten bir GraphQL motoru olan "Private GraphQL API" üzerinde çalışıyorum. Bu motor, WordPress plugin geliştiricilerinin Gutenberg blokları için bir GraphQL API sunan kendi plugin'lerine dahil etmeleri amacıyla kullanılabilecek.

Gato GraphQL plugin'inden gerekli olmayan paketleri (UI, istemciler, özel endpoint'ler, HTTP önbellekleme, persisted queries ve diğer birkaçıyla ilgilenenler) kaldırarak "Private GraphQL API"yi zahmetsizce oluşturabilirim.

Son olarak, kapsamlandırmanın kolay olması sayesinde (yukarıda görüldüğü gibi), gerekli tüm paketlere önek ekleyebilirim; böylece Private GraphQL API çakışmalar olmaksızın çalışacaktır (bu durum, 2 farklı plugin Private GraphQL API'nin farklı sürümlerini dahil ettiğinde ortaya çıkabilir).

Bu yaklaşımın dezavantajları

Söylemek gerekmez ki bu yaklaşım mükemmel değildir.

Daha fazla çaba gerektirir, kod daha ayrıntılı hale gelir

Normalde, uygulamamız WordPress üzerinde çalışıyorsa bir gönderi listesi almak için yalnızca get_posts'u çalıştırırız. Basit ve kolay.

CMS-agnostic hale getirmek işleri önemli ölçüde karmaşık hale getirir. Bir gönderi listesi almak için şunları yapmamız gerekir:

  • posts ve posts-wp paketleri oluşturmak
  • posts paketinde getPosts işlevini içeren bir sözleşme oluşturmak
  • posts-wp paketinde get_posts aracılığıyla sözleşmeyi karşılamak
  • İşlevselliği her zaman sözleşme aracılığıyla çağırdığımızdan emin olmak, asla doğrudan değil

(Büyük olasılıkla) bağımlılık enjeksiyonu gerektirir

CMS-agnostic paketteki her sözleşmeyi ve CMS'e özgü paketteki uygulamasını bağlamamız gerekir. Benim durumumda, Symfony'nin DependencyInjection bileşeni tarafından sağlanan bir hizmet konteyneri kullanıyorum.

Bu yaklaşımı seviyorum; uygulamayı büyük ölçüde basitleştirdiğine inanıyorum. Ancak her uygulamanın bağımlılık enjeksiyonu gerektirmeyeceğini anlıyorum; bu da uygulamaya karmaşıklık katabilir.

(Büyük olasılıkla) bir monorepo gerektirir

Gato GraphQL sonunda 91 paket içerdi. Geçmişte bunların her birini kendi deposunda barındırıyordum; bu da PR oluşturmayı çok zorlaştırıyordu. Bu yüzden monorepo yaklaşımına geçmeye "zorlandım".

Açık olmak gerekirse: monorepo'yu gerçekten seviyorum. Ama herkesin sevmediğini anlıyorum ve kendi başına bakım çabası da gerektiriyor.

Faydalı bağlantılar

Daha önce WordPress web sitemi soyutlama ve CMS-agnostic hale getirme motivasyonlarım ve stratejim hakkında yazmıştım. Bu, Gato GraphQL için kod tabanını bölmek amacıyla uyguladığım stratejinin aynısıdır:

Ek: Plugin'i oluşturan 91 paketin listesi

Gato GraphQL şu 91 paketi içerir.

Motor işlevselliği:

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 işlevselliği:

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

GraphQL sunucu işlevselliği:

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

Veri modeli:

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

Bültenimize abone olun

Gato GraphQL'deki tüm güncellemelerden haberdar olun.