đđœââïž Warum Gato GraphQL fĂŒr CMS-Agnostizismus in ~90 Packages aufgeteilt wurde â und die Vor- und Nachteile dieses Ansatzes
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:
- PoP, eine serverseitige Komponenten-Modell-Bibliothek (wie React, aber fĂŒr das Back-end)
- GraphQL by PoP, ein CMS-agnostischer GraphQL-Server fĂŒr PHP
- Gato GraphQL
- ein Site-Builder (WIP)
- 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
postsundposts-wperstellen - Einen Vertrag mit der Funktion
getPostsim Packagepostserstellen - Den Vertrag ĂŒber
get_postsim Packageposts-wperfĂŒ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.
NĂŒtzliche Links
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:
- Abstracting WordPress Code To Reuse With Other CMSs: Concepts (Part 1)
- Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)
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