Blog

đŸ’đŸœâ€â™‚ïž Warum Gato GraphQL fĂŒr CMS-Agnostizismus in ~90 Packages aufgeteilt wurde – und die Vor- und Nachteile dieses Ansatzes

Leonardo Losoviz
Von Leonardo Losoviz ·

Letzte Woche habe ich den Artikel đŸ’đŸ»â€â™€ïž Warum Gato GraphQL ein Monorepo braucht und wie es optimiert ist veröffentlicht, in dem ich erklĂ€re, wie und warum das GatoGraphQL/GatoGraphQL-Monorepo, das den Code fĂŒr Gato GraphQL enthĂ€lt, die Codebase des Plugins effizient verwalten kann.

Ich habe meinen Artikel auf Reddit geteilt und erhielt den folgenden Kommentar:

Der Artikel des OP und die verlinkten Artikel lesen sich, als wĂ€re ein Monorepo die grĂ¶ĂŸte Erfindung seit dem Schneidebrot.

Ein interessanterer Artikel wĂ€re zu erklĂ€ren, warum du dachtest, dass CMS-Agnostizismus es erfordert, alles in sein eigenes kleines Package aufzuteilen, und warum du glaubtest, dass jedes der ĂŒber 200 Packages von Anfang an in seinem eigenen Repo sein mĂŒsste.

Das ist eine interessante Frage. Ich habe daher beschlossen, diesen Artikel zu schreiben, um sie etwas ausfĂŒhrlicher zu beantworten.

ZunÀchst gehe ich jedoch auf zwei verwandte Themen ein: wie viele Packages das Plugin tatsÀchlich benötigt und warum ich behaupte, dass der zugrunde liegende GraphQL-Server CMS-agnostisch ist.

Wie viele Packages das Plugin ausmachen

Auch wenn ich ĂŒber 200 PHP-Packages erwĂ€hnt habe, gilt das fĂŒr das Monorepo; fĂŒr das Plugin sind es tatsĂ€chlich deutlich weniger.

Das Monorepo GatoGraphQL/GatoGraphQL umfasst 5 Projekte:

  1. PoP, eine serverseitige Komponenten-Modell-Bibliothek (wie React, aber fĂŒr das Back-end)
  2. GraphQL by PoP, ein CMS-agnostischer GraphQL-Server fĂŒr PHP
  3. Gato GraphQL
  4. ein Site-Builder (WIP)
  5. Wassup, ein Website-Theme basierend auf dem Site-Builder (WIP)

Diese Projekte in einem Monorepo zu hosten vereinfacht die Arbeit damit, wegen ihrer gegenseitigen AbhÀngigkeiten:

  • GraphQL by PoP basiert auf PoP
  • Gato GraphQL basiert auf GraphQL by PoP
  • Der Site-Builder nutzt die Komponenten-Modell-Bibliothek als seine Engine (Ă€hnlich wie Gatsby GraphQL verwendet)
  • Wassup basiert auf dem Site-Builder

Den Code aller 5 Projekte betreffend enthĂ€lt GatoGraphQL/GatoGraphQL ĂŒber 200 PHP-Packages. Gato GraphQL betreffend sind es „nur" 91 Packages. Und GraphQL by PoP, der zugrunde liegende GraphQL-Server, enthĂ€lt „nur" 98 Packages.

(Das Gato GraphQL Plugin benötigt weniger Packages als sein zugrunde liegender GraphQL-Server, weil einige Packages, wie die Google Translate @strTranslate-Direktive, noch nicht zum Plugin hinzugefĂŒgt wurden.)

Wie ist GraphQL by PoP CMS-agnostisch? Wie unterscheidet es sich von webonyx?

Ich habe gesagt, dass GraphQL by PoP CMS-agnostisch ist. Aber was bedeutet das?

Nun, auch webonyx/graphql-php ist CMS-agnostisch. Wie unterscheiden sie sich also?

webonyx/graphql-php ist CMS-agnostisch, da es ein ĂŒber Composer verteiltes Package ist, das nur „vanilla" PHP-Code enthĂ€lt. Es ist jedoch kein eigenstĂ€ndiger GraphQL-Server; vielmehr ist es eine PHP-Implementierung der GraphQL-Spezifikation, die in einen GraphQL-Server in PHP eingebettet werden soll.

Diese implementierenden GraphQL-Server, wie Lighthouse oder WPGraphQL, sind nicht CMS-agnostisch. Wir können Lighthouse nicht auf WordPress betreiben oder WPGraphQL auf Laravel.

In diesem Sinne ist GraphQL by PoP CMS-agnostisch: Es ist der „fast-fertige" GraphQL-Server, fast bereit, mit jedem CMS oder Framework zu laufen, sei es Laravel, WordPress oder ein anderes. (Der KĂŒrze halber bedeutet „CMS" ab jetzt immer „CMS oder Framework".)

Um es fĂŒr ein bestimmtes CMS abschließend zu machen, benötigt der GraphQL-Server noch etwas benutzerdefinierten Code fĂŒr dieses CMS, ĂŒber ein entsprechendes Package.

Ich gehe nun auf die Fragen des Kommentars ein.

Warum jedes Package in seinem eigenen Repo sein musste

Weil Packagist (Composers Registry fĂŒr PHP-Packages) es erfordert, eine Repository-URL anzugeben, um ein Package zu veröffentlichen/zu verteilen.

(Übrigens spricht mein Artikel Hosting all your PHP packages together in a monorepo, ebenfalls letzte Woche veröffentlicht, ĂŒber dieses Thema.)

Warum CMS-Agnostizismus es erfordert, alles in sein eigenes kleines Package aufzuteilen

Es gibt einige GrĂŒnde dafĂŒr.

Das CMS seinen eigenen Code injizieren lassen

Es ist unmöglich, einen GraphQL-Server zu erstellen, der ĂŒberall mit 100% demselben PHP-Code funktioniert.

Um beispielsweise beliebigen Code zu ermöglichen, den Wert einer Variablen anderswo zu Ă€ndern, verlĂ€sst sich WordPress auf Filter-Hooks, Symfony verwendet die EventDispatcher-Komponente, und Laravel hat sein eigenes System von Events und Listeners. Der PHP-Code fĂŒr diese 3 verschiedenen Methoden wird ebenfalls unterschiedlich sein.

Hier kommt der Ansatz ins Spiel, den Code in granulare Packages aufzuteilen. Anstatt eine Lösung fĂŒr Events und Listeners Teil der Anwendung sein zu lassen, wird sie ĂŒber ein Package in die Anwendung injiziert, und dieses Package enthĂ€lt Code, der spezifisch fĂŒr das CMS ist.

Damit das funktioniert, muss jede FunktionalitÀt in 2 Packages aufgeteilt werden:

  • ein CMS-agnostisches Package, das die gesamte Business-Logik enthĂ€lt und nur „vanilla" PHP-Code verwendet. Dieses Package enthĂ€lt die VertrĂ€ge, die vom CMS-spezifischen Package zu erfĂŒllen sind
  • ein CMS-spezifisches Package, das die VertrĂ€ge fĂŒr dieses CMS erfĂŒllt

GraphQL by PoP hat beispielsweise ein Package hooks, das den folgenden Vertrag enthÀlt:

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

Und dann erfĂŒllt das Package hooks-wp den Vertrag fĂŒr 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);
  }
}

Obwohl das Konzept der Hooks aus WordPress stammt, kann es auch mit anderen CMSs funktionieren (zum Beispiel durch Verwendung von Events und Listeners zur Implementierung von Hooks). Wir können dann hooks-wp durch hooks-laravel, hooks-symfony, hooks-drupal, hooks-octobercms oder ein anderes ersetzen, um die VertrĂ€ge mit dem fĂŒr jedes CMS spezifischen Code zu erfĂŒllen.

Dem CMS erlauben, FunktionalitĂ€ten zu verwerfen, die es nicht unterstĂŒtzen kann

Nicht alle CMSs können alle FunktionalitĂ€ten unterstĂŒtzen. WordPress ermöglicht es beispielsweise, BeitrĂ€ge nach einem meta_value-Eintrag zu sortieren, OctoberCMS hingegen nicht.

Deshalb enthĂ€lt GraphQL by PoP das Package metaquery (fĂŒr WordPress ĂŒber metaquery-wp erfĂŒllt). Der fĂŒr WordPress implementierte GraphQL-Server enthĂ€lt dieses Package dann, der fĂŒr OctoberCMS hingegen nicht.

Vorteile dieses Ansatzes

Unsere Packages granular aufzuteilen bietet einige Vorteile.

Business-Logik von CMS-spezifischem Code entkoppeln

Anstatt die Anwendung auf Basis der Eigenmeinungen (Codierweise, Features, EinschrÀnkungen und anderes) eines CMS zu programmieren, können wir unseren Code abstrahieren und nur Business-Logik verwenden.

Um eine Liste von BeitrĂ€gen abzurufen, kann die Anwendung beispielsweise die Methode getPosts aus einem Interface in einem CMS-agnostischen Package posts ausfĂŒhren. BeitrĂ€ge werden dann immer auf dieselbe Weise abgerufen, unabhĂ€ngig von der Implementierung des zugrunde liegenden CMS.

Technische Schulden umgehen und die neuesten Standards verwenden

Dem obigen Beispiel folgend rufen wir unsere BeitrĂ€ge ab, indem wir die Methode getPosts ausfĂŒhren, die der PSR-4-Konvention folgt, anstatt get_posts aufzurufen, wie es von WordPress definiert wird.

Ebenso können wir getCustomPost ausfĂŒhren, um einen Custom Post abzurufen, anstatt das ungenaue get_post (das ist Teil von WordPress' technischer Schuld).

Es ist einfach zu scoppen

PHP-Scoper zum Scoppen eines WordPress-Plugins zu verwenden ist nicht einfach, und selbst wenn es machbar ist, ist es fehleranfÀllig.

Den CMS-spezifischen Code und die Business-Logik der Anwendung vollstÀndig entkoppelt zu halten ermöglicht es, PHP-Scoper auf nur einen Satz von Packages anzuwenden (die mit der Business-Logik) und es bei den anderen (die WordPress-Code enthalten) zu vermeiden. Ich habe diese Strategie im Detail beschrieben, hier.

Außerdem kann es Ă€hnlich wie PHP-Scoper andere Tools geben, die bei Anwendung auf CMS-spezifischen Code (wie WordPress) versagen. In diesen FĂ€llen kann die granulare Aufteilung der Packages die Situation retten.

Wir können verschiedene Anwendungen produzieren, die jeweils nur den Code enthalten, den sie brauchen

Wir können unsere Packages wiederverwenden, um weitere Anwendungen zu erstellen, die nur die benötigten Packages und nichts sonst enthalten.

Ein persönlicher Blog beispielsweise benötigt möglicherweise nur posts, tags und categories, sodass er sich nicht mit FunktionalitĂ€ten fĂŒr users oder user-login befassen muss.

TatsĂ€chlich plane ich, von diesem Feature bald zu profitieren: Ich arbeite derzeit an der „Private GraphQL API", einer eigenstĂ€ndigen GraphQL-Engine, die WordPress-Plugin-Entwicklern zur VerfĂŒgung gestellt werden soll, damit sie sie in ihre Plugins einbetten können und eine GraphQL-API fĂŒr ihre Gutenberg-Blöcke erhalten.

Ich kann die „Private GraphQL API" mĂŒhelos erstellen, indem ich einfach die Packages aus dem Gato GraphQL Plugin entferne, die nicht benötigt werden (die fĂŒr UI, Clients, Custom Endpoints, HTTP-Caching, persisted queries und einige andere zustĂ€ndig sind).

Da es außerdem einfach zu scoppen ist (wie oben gesehen), kann ich alle erforderlichen Packages mit einem PrĂ€fix versehen, sodass die Private GraphQL API ohne Konflikte funktioniert (was passieren könnte, wenn 2 verschiedene Plugins unterschiedliche Versionen der Private GraphQL API einbetten).

Nachteile dieses Ansatzes

NatĂŒrlich ist dieser Ansatz alles andere als perfekt.

GrĂ¶ĂŸerer Aufwand, der Code wird ausfĂŒhrlicher

Wenn unsere Anwendung auf WordPress lĂ€uft, fĂŒhren wir normalerweise einfach get_posts aus, um eine Liste von BeitrĂ€gen abzurufen. Einfach und unkompliziert.

CMS-agnostisch zu machen verkompliziert die Sache erheblich. Um eine Liste von BeitrĂ€gen abzurufen, mĂŒssen wir:

  • Packages posts und posts-wp erstellen
  • Einen Vertrag mit der Funktion getPosts im Package posts erstellen
  • Den Vertrag ĂŒber get_posts im Package posts-wp erfĂŒllen
  • Immer darauf achten, die FunktionalitĂ€t ĂŒber den Vertrag aufzurufen, niemals direkt

Es erfordert (sehr wahrscheinlich) Dependency Injection

Wir mĂŒssen jeden Vertrag des CMS-agnostischen Packages und seine Implementierung des CMS-spezifischen Packages binden. In meinem Fall verwende ich einen Service-Container, bereitgestellt von Symfonys DependencyInjection-Komponente.

Ich liebe diesen Ansatz und glaube, er vereinfacht die Anwendung erheblich. Ich verstehe jedoch, dass nicht jede Anwendung sonst Dependency Injection benötigen wĂŒrde, was ihr KomplexitĂ€t hinzufĂŒgt.

Es erfordert (höchstwahrscheinlich) ein Monorepo

Gato GraphQL enthĂ€lt am Ende 91 Packages. In der Vergangenheit habe ich jedes von ihnen in seinem eigenen Repository gehostet, was das Erstellen von PRs sehr schwierig gemacht hat. Ich war daher „gezwungen", zum Monorepo-Ansatz zu wechseln.

Um es klar zu sagen: Ich mag das Monorepo wirklich. Ich verstehe aber, dass nicht jeder es mag und es auch seinen eigenen Wartungsaufwand erfordert.

Ich habe zuvor ĂŒber meine Motivationen und meine Strategie geschrieben, meine WordPress-Website zu abstrahieren und CMS-agnostisch zu machen. Es ist dieselbe Strategie, die ich angewendet habe, um die Codebase fĂŒr Gato GraphQL aufzuteilen:

Anhang: Liste der 91 Packages, die das Plugin ausmachen

Gato GraphQL enthÀlt die folgenden 91 Packages.

Engine-FunktionalitÀt:

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-FunktionalitÀt:

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

GraphQL-Server-FunktionalitÀt:

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

Datenmodell:

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

Abonniere unseren Newsletter

Bleib ĂŒber alle Updates zu Gato GraphQL auf dem Laufenden.