Blog

💁🏽‍♂️ Dlaczego aby wspierać CMS-agnostycyzm, Gato GraphQL został podzielony na ~90 pakietów, oraz zalety i wady tego podejścia

Leonardo Losoviz
Autor: Leonardo Losoviz ·

W zeszłym tygodniu opublikowałem artykuł 💁🏻‍♀️ Dlaczego Gato GraphQL potrzebuje monorepo i jak jest ono zoptymalizowane, wyjaśniający jak i dlaczego monorepo GatoGraphQL/GatoGraphQL, które hostuje kod Gato GraphQL, potrafi efektywnie zarządzać bazą kodu pluginu.

Podzieliłem się swoim artykułem na Reddit i otrzymałem następujący komentarz:

Artykuł autora oraz artykuły, do których się odnosi, sprawiają wrażenie, jakby monorepo było największym wynalazkiem od czasu krojonego chleba.

Ciekawszym artykułem byłoby wyjaśnienie, dlaczego uznałeś, że CMS-agnostycyzm wymaga podzielenia wszystkiego na własne małe pakiety, i dlaczego uważałeś, że każdy z ponad 200 pakietów musi od samego początku znajdować się we własnym repozytorium.

To interesujące pytanie. Postanowiłem więc napisać ten artykuł, żeby odpowiedzieć na nie nieco dokładniej.

Ale najpierw omówię dwa powiązane tematy: ile pakietów jest faktycznie wymaganych przez plugin, i dlaczego twierdzę, że bazowy serwer GraphQL jest CMS-agnostic.

Ile pakietów składa się na plugin

Choć wspominałem o ponad 200 pakietach PHP, dotyczy to monorepo; w przypadku pluginu jest ich znacznie mniej.

Monorepo GatoGraphQL/GatoGraphQL obejmuje 5 projektów:

  1. PoP, biblioteka modelu komponentów po stronie serwera (jak React, ale dla back-endu)
  2. GraphQL by PoP, CMS-agnostic serwer GraphQL dla PHP
  3. Gato GraphQL
  4. konstruktor stron (WIP)
  5. Wassup, motyw strony oparty na konstruktorze stron (WIP)

Hostowanie tych projektów w monorepo upraszcza pracę z nimi ze względu na ich wzajemne zależności:

  • GraphQL by PoP jest oparty na PoP
  • Gato GraphQL jest oparty na GraphQL by PoP
  • Konstruktor stron używa biblioteki modelu komponentów jako silnika (podobnie jak Gatsby używa GraphQL)
  • Wassup jest oparty na konstruktorze stron

To w odniesieniu do kodu wszystkich 5 projektów GatoGraphQL/GatoGraphQL zawiera ponad 200 pakietów PHP. Jeśli chodzi o Gato GraphQL, jest ich "tylko" 91. A GraphQL by PoP, bazowy serwer GraphQL, zawiera "tylko" 98 pakietów.

(Plugin Gato GraphQL wymaga mniej pakietów niż jego bazowy serwer GraphQL, ponieważ niektóre pakiety, takie jak dyrektywa @strTranslate Google Translate, nie zostały jeszcze dodane do pluginu.)

Jak GraphQL by PoP jest CMS-agnostic? Czym różni się od webonyx?

Mówiłem, że GraphQL by PoP jest CMS-agnostic. Ale co to oznacza?

Przy okazji, webonyx/graphql-php również jest CMS-agnostic. Czym więc się różnią?

webonyx/graphql-php jest CMS-agnostic w tym sensie, że jest to pakiet dystrybuowany przez Composer, zawierający wyłącznie "vanilla" PHP. Jednak sam w sobie nie jest serwerem GraphQL; jest implementacją specyfikacji GraphQL w PHP, przeznaczoną do osadzenia w jakimś serwerze GraphQL w PHP.

Teraz, te serwery GraphQL, które go implementują, jak Lighthouse czy WPGraphQL, nie są CMS-agnostic. Nie możemy uruchomić Lighthouse na WordPress, ani WPGraphQL na Laravel.

W tym właśnie sensie GraphQL by PoP jest CMS-agnostic: jest "prawie gotowym" serwerem GraphQL, niemal gotowym do działania z dowolnym CMS lub frameworkiem, czy to Laravel, WordPress, czy jakimkolwiek innym. (Dla zwięzłości, od tej pory, ilekroć powiem "CMS", mam na myśli "CMS lub framework".)

Aby stał się ostateczny dla konkretnego CMS, serwer GraphQL nadal będzie potrzebował pewnego kodu specyficznego dla tego CMS, za pośrednictwem odpowiedniego pakietu.

Odpowiem teraz na pytania z komentarza.

Dlaczego każdy pakiet musiał znajdować się we własnym repozytorium

Ponieważ Packagist (rejestr pakietów PHP Composera) wymaga podania adresu URL repozytorium do publikowania/dystrybucji pakietu.

(Przy okazji, mój artykuł Hosting all your PHP packages together in a monorepo, również opublikowany w zeszłym tygodniu, mówi o tym problemie.)

Dlaczego CMS-agnostycyzm wymaga podziału wszystkiego na własne małe pakiety

Jest kilka powodów.

Pozwól CMS wstrzyknąć własny kod

Nie jest możliwe stworzenie serwera GraphQL, który działa wszędzie, używając 100% tego samego kodu PHP.

Na przykład, aby umożliwić dowolnemu fragmentowi kodu modyfikację wartości jakiejś zmiennej w innym miejscu, WordPress korzysta z filter hooks, Symfony używa komponentu EventDispatcher, a Laravel ma własny system zdarzeń i listenerów. Kod PHP dla tych 3 różnych metod również będzie różny.

Tu właśnie wchodzi w grę podejście polegające na podziale kodu na granularne pakiety. Zamiast rozwiązanie dla zdarzeń i listenerów stanowiło część aplikacji, jest ono wstrzykiwane do aplikacji przez pakiet, a ten pakiet zawiera kod specyficzny dla danego CMS.

Aby to działało, każda funkcjonalność musi zostać podzielona na 2 pakiety:

  • pakiet CMS-agnostic, zawierający całą logikę biznesową, używający wyłącznie "vanilla" PHP. Ten pakiet będzie zawierał kontrakty do spełnienia przez pakiet specyficzny dla CMS
  • pakiet specyficzny dla CMS, spełniający kontrakty dla tego CMS

Na przykład, GraphQL by PoP ma pakiet hooks zawierający następujący kontrakt:

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

A następnie pakiet hooks-wp spełnia kontrakt dla 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);
  }
}

Teraz, choć koncepcja hooków pochodzi z WordPress, może działać również z innymi CMS-ami (na przykład za pomocą zdarzeń i listenerów do implementacji hooków). Następnie możemy zastąpić hooks-wp przez hooks-laravel, hooks-symfony, hooks-drupal, hooks-octobercms lub dowolny inny, aby spełnić kontrakty przy użyciu kodu specyficznego dla każdego CMS.

Pozwól CMS odrzucić funkcjonalności, których nie może obsługiwać

Nie wszystkie CMS-y mogą obsługiwać wszystkie funkcjonalności. Na przykład, WordPress umożliwia sortowanie postów według wpisu meta_value, ale OctoberCMS nie.

Dlatego GraphQL by PoP zawiera pakiet metaquery (spełniany dla WordPress przez metaquery-wp). Serwer GraphQL zaimplementowany dla WordPress będzie zawierał ten pakiet, ale ten przeznaczony dla OctoberCMS — nie.

Zalety tego podejścia

Granularny podział pakietów oferuje kilka korzyści.

Oddzielenie logiki biznesowej od kodu specyficznego dla CMS

Zamiast kodować aplikację w oparciu o opinionatedness (sposób kodowania, funkcjonalności, ograniczenia i inne aspekty) danego CMS, możemy abstrakcyjnie podejść do naszego kodu i używać wyłącznie logiki biznesowej.

Na przykład, aby pobrać listę postów, aplikacja może wykonać metodę getPosts z pewnego interfejsu w CMS-agnostic pakiecie posts. W ten sposób posty będą zawsze pobierane w ten sam sposób, niezależnie od implementacji przez bazowy CMS.

Ominięcie długu technicznego i stosowanie najnowszych standardów

Kontynuując powyższy przykład, pobieramy nasze posty wykonując metodę getPosts, która jest zgodna z konwencją PSR-4, zamiast wywoływać get_posts zgodnie z definicją WordPress.

Podobnie możemy wykonać getCustomPost, aby pobrać custom post, zamiast niedokładnego get_post (to część długu technicznego WordPress).

Łatwy do scope'owania

Użycie PHP-Scoper do scope'owania pluginu WordPress nie jest łatwe, a nawet gdy jest możliwe, jest podatne na błędy.

Zachowanie kodu specyficznego dla CMS i logiki biznesowej aplikacji w pełni rozdzielonych pozwala zastosować PHP-Scoper tylko na jednym zestawie pakietów (tych zawierających logikę biznesową), a uniknąć go w pozostałych (tych zawierających kod WordPress). Opisałem tę strategię szczegółowo tutaj.

Ponadto, podobnie jak w przypadku PHP-Scoper, mogą istnieć inne narzędzia, które zawodzą po zastosowaniu na kodzie specyficznym dla CMS (jak WordPress). W takich przypadkach granularny podział pakietów może okazać się ratunkiem.

Możemy tworzyć różne aplikacje, każda zawierająca tylko potrzebny kod

Możemy ponownie wykorzystać nasze pakiety do tworzenia kolejnych aplikacji, zawierających tylko te pakiety, których każda potrzebuje, i nic więcej.

Na przykład, blog osobisty może potrzebować tylko posts, tags i categories, dzięki czemu może uniknąć obsługi funkcjonalności dla users lub user-login.

W rzeczy samej, planuję wkrótce skorzystać z tej funkcjonalności: aktualnie pracuję nad "Private GraphQL API", autonomicznym silnikiem GraphQL, który ma być udostępniony deweloperom pluginów WordPress do dołączenia do ich pluginów, zapewniając GraphQL API dla ich bloków Gutenberg.

Mogę bez wysiłku stworzyć "Private GraphQL API", po prostu usuwając z pluginu Gato GraphQL pakiety, które nie są potrzebne (te odpowiadające za UI, klientów, custom endpoints, cache HTTP, utrwalone queries i kilka innych).

Wreszcie, ponieważ łatwo go scope'ować (jak widać powyżej), mogę prefiksować wszystkie wymagane pakiety, dzięki czemu Private GraphQL API będzie działać bez konfliktów (które mogłyby wystąpić, gdy 2 różne pluginy dołączają różne wersje Private GraphQL API).

Wady tego podejścia

Nie trzeba mówić, że to podejście jest dalekie od doskonałości.

Większy wysiłek, kod staje się bardziej rozwlekły

Zazwyczaj, jeśli nasza aplikacja działa na WordPress, żeby pobrać listę postów, po prostu wykonujemy get_posts. Prosto i łatwo.

Uczynienie tego CMS-agnostic znacznie komplikuje sprawę. Aby pobrać listę postów, musimy:

  • Stworzyć pakiety posts i posts-wp
  • Stworzyć kontrakt z funkcją getPosts w pakiecie posts
  • Spełnić kontrakt przez get_posts w pakiecie posts-wp
  • Zawsze upewnić się, że funkcjonalność jest wywoływana przez kontrakt, nigdy bezpośrednio

(Bardzo prawdopodobnie) wymaga wstrzykiwania zależności

Musimy powiązać każdy kontrakt z pakietu CMS-agnostic z jego implementacją z pakietu specyficznego dla CMS. W moim przypadku używam kontenera usług, dostarczonego przez komponent DependencyInjection Symfony.

Uwielbiam to podejście, uważam, że znacznie upraszcza aplikację. Rozumiem jednak, że nie każda aplikacja wymagałaby w innym razie wstrzykiwania zależności, co dodaje jej złożoności.

(Najprawdopodobniej) wymaga monorepo

Gato GraphQL skończył z 91 pakietami. W przeszłości hostowałem każdy z nich we własnym repozytorium, co bardzo utrudniało tworzenie PR-ów. Zostałem więc "zmuszony" do przyjęcia podejścia z monorepo.

Żeby być precyzyjnym: naprawdę lubię monorepo. Rozumiem jednak, że nie wszyscy je lubią, i wymaga ono własnego nakładu pracy w utrzymaniu.

Przydatne linki

Wcześniej pisałem o moich motywacjach i strategii abstrahowania mojej witryny WordPress, czyniąc ją CMS-agnostic. To ta sama strategia, którą zastosowałem do podziału bazy kodu Gato GraphQL:

Addendum: Lista 91 pakietów składających się na plugin

Gato GraphQL zawiera następujące 91 pakietów.

Funkcjonalności silnika:

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

Funkcjonalności API:

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

Funkcjonalności serwera 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 danych:

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

Zapisz się do naszego newslettera

Bądź na bieżąco ze wszystkimi aktualizacjami Gato GraphQL.