Blog

πŸ’πŸ½β€β™‚οΈ Waarom CMS-agnosticisme ondersteunen, Gato GraphQL opgesplitst in ~90 pakketten, en de voor- en nadelen van deze aanpak

Leonardo Losoviz
Door Leonardo Losoviz Β·

Vorige week publiceerde ik het artikel πŸ’πŸ»β€β™€οΈ Waarom Gato GraphQL een Monorepo nodig heeft en hoe het geoptimaliseerd is, waarin ik uitleg hoe en waarom de GatoGraphQL/GatoGraphQL monorepo, die de code voor Gato GraphQL herbergt, de codebase van de plugin efficiΓ«nt kan beheren.

Ik deelde mijn artikel op Reddit en kreeg de volgende reactie:

Het artikel van de OP en de artikelen waarnaar het linkt, lezen een beetje alsof een monorepo het grootste ding is sinds gesneden brood.

Een interessanter artikel zou zijn om uit te leggen waarom je dacht dat CMS-agnosticisme vereist dat alles in zijn eigen kleine pakket wordt opgesplitst, en waarom je dacht dat elk van de meer dan 200 pakketten van meet af aan in zijn eigen repo moest staan.

Dat is een interessante vraag. Daarom besloot ik dit artikel te schrijven, om er wat verder op in te gaan.

Maar eerst bespreek ik twee gerelateerde onderwerpen: hoeveel pakketten de plugin eigenlijk nodig heeft, en waarom ik beweer dat de onderliggende GraphQL-server CMS-agnostic is.

Hoeveel pakketten de plugin omvat

Hoewel ik het had over meer dan 200 PHP-pakketten, geldt dat voor de monorepo; voor de plugin is het eigenlijk veel minder.

De GatoGraphQL/GatoGraphQL monorepo omvat 5 projecten:

  1. PoP, een server-side componentmodelbibliotheek (zoals React, maar voor de back-end)
  2. GraphQL by PoP, een CMS-agnostic GraphQL-server voor PHP
  3. Gato GraphQL
  4. een site builder (WIP)
  5. Wassup, een websitethema gebaseerd op de site builder (WIP)

Het hosten van deze projecten in een monorepo vereenvoudigt het werken ermee, vanwege hun onderlinge afhankelijkheden:

  • GraphQL by PoP is gebaseerd op PoP
  • Gato GraphQL is gebaseerd op GraphQL by PoP
  • De site builder gebruikt de componentmodelbibliotheek als engine (vergelijkbaar met hoe Gatsby GraphQL gebruikt)
  • Wassup is gebaseerd op de site builder

Het is met betrekking tot de code van alle 5 projecten dat GatoGraphQL/GatoGraphQL meer dan 200 PHP-pakketten bevat. Voor Gato GraphQL zijn dat er "slechts" 91. En GraphQL by PoP, de onderliggende GraphQL-server, bevat "slechts" 98 pakketten.

(De Gato GraphQL-plugin heeft minder pakketten nodig dan de onderliggende GraphQL-server, omdat sommige pakketten, zoals de Google Translate @strTranslate-directive, nog niet aan de plugin zijn toegevoegd.)

Hoe is GraphQL by PoP CMS-agnostic? Hoe verschilt het van webonyx?

Ik heb gezegd dat GraphQL by PoP CMS-agnostic is. Maar wat betekent dat?

Trouwens, webonyx/graphql-php is ook CMS-agnostic. Hoe verschillen ze dan?

webonyx/graphql-php is CMS-agnostic in de zin dat het een pakket is dat via Composer wordt gedistribueerd en alleen "vanilla" PHP-code bevat. Het is echter op zichzelf geen GraphQL-server; het is een implementatie in PHP van de GraphQL-specificatie, om te worden ingebed in een GraphQL-server in PHP.

Nu zijn deze implementerende GraphQL-servers, zoals Lighthouse of WPGraphQL, niet CMS-agnostic. We kunnen Lighthouse niet op WordPress draaien, of WPGraphQL op Laravel.

In die zin is GraphQL by PoP CMS-agnostic: het is de "bijna-definitieve" GraphQL-server, bijna klaar om te draaien met elk CMS of framework, of dat nu Laravel, WordPress of iets anders is. (Ter wille van de beknoptheid bedoel ik met "CMS" voortaan "CMS of framework".)

Om het definitief te maken voor een bepaald CMS, heeft de GraphQL-server nog steeds aangepaste code nodig voor dat CMS, via een bijbehorend pakket.

Ik beantwoord nu de vragen uit de reactie.

Waarom elk pakket in zijn eigen repo moest staan

Omdat Packagist (het register van PHP-pakketten van Composer) vereist dat je een repository-URL opgeeft voor het publiceren/distribueren van een pakket.

(Trouwens, mijn artikel Hosting all your PHP packages together in a monorepo, ook vorige week gepubliceerd, gaat over dit probleem.)

Waarom CMS-agnosticisme vereist dat alles wordt opgesplitst in eigen kleine pakketten

Er zijn een paar redenen.

Laat het CMS zijn eigen code injecteren

Het is onmogelijk om een GraphQL-server te maken die overal werkt met 100% dezelfde PHP-code.

Zo vertrouwt WordPress op filter hooks om elk stuk code in staat te stellen de waarde van een variabele ergens anders te wijzigen, gebruikt Symfony de EventDispatcher-component, en heeft Laravel zijn eigen systeem van events en listeners. De PHP-code voor deze 3 verschillende methoden zal ook verschillend zijn.

Hier komt de aanpak van het opsplitsen van de code in granulaire pakketten om de hoek kijken. In plaats van een oplossing voor events en listeners onderdeel te laten zijn van de applicatie, wordt deze via een pakket in de applicatie geΓ―njecteerd, en dit pakket bevat code die specifiek is voor het CMS.

Hiervoor moet elke functionaliteit worden opgesplitst in 2 pakketten:

  • een CMS-agnostic pakket, met alle bedrijfslogica, dat alleen "vanilla" PHP-code gebruikt. Dit pakket bevat de contracten die door het CMS-specifieke pakket moeten worden nagekomen
  • een CMS-specifiek pakket, dat de contracten voor dat CMS nakomt

GraphQL by PoP heeft bijvoorbeeld een pakket hooks met het volgende contract:

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

En vervolgens voldoet pakket hooks-wp aan het contract voor 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);
  }
}

Hoewel het concept van hooks afkomstig is uit WordPress, kan het ook werken met andere CMSs (bijvoorbeeld door events en listeners te gebruiken om hooks te implementeren). We kunnen hooks-wp dan vervangen door hooks-laravel, hooks-symfony, hooks-drupal, hooks-octobercms of een ander pakket, om de contracten na te komen met de code die specifiek is voor elk CMS.

Laat het CMS functionaliteit verwijderen die het niet kan ondersteunen

Niet alle CMSs kunnen alle functionaliteit ondersteunen. WordPress maakt het bijvoorbeeld mogelijk om berichten te sorteren op een meta_value-invoer, maar OctoberCMS niet.

Daarom bevat GraphQL by PoP het pakket metaquery (voor WordPress voldaan via metaquery-wp). De GraphQL-server die voor WordPress is geΓ―mplementeerd, bevat dit pakket, maar die voor OctoberCMS niet.

Voordelen van deze aanpak

Het granulaire opsplitsen van pakketten biedt een aantal voordelen.

Bedrijfslogica loskoppelen van CMS-specifieke code

In plaats van de applicatie te coderen op basis van de eigenzinnigheid (manier van coderen, functies, beperkingen en andere aspecten) van een CMS, kunnen we onze code abstraheren en alleen bedrijfslogica gebruiken.

Om bijvoorbeeld een lijst van berichten op te halen, kan de applicatie de methode getPosts uitvoeren vanuit een interface in een CMS-agnostic pakket posts. De berichten worden dan altijd op dezelfde manier opgehaald, ongeacht de implementatie door het onderliggende CMS.

Technische schuld omzeilen en de nieuwste standaarden gebruiken

Voortbordurend op het bovenstaande voorbeeld halen we onze berichten op door de methode getPosts uit te voeren, die de PSR-4-conventie volgt, in plaats van get_posts aan te roepen zoals gedefinieerd door WordPress.

Op dezelfde manier kunnen we getCustomPost uitvoeren om een custom post op te halen, in plaats van het onnauwkeurige get_post (dit is onderdeel van de technische schuld van WordPress).

Eenvoudig te scoppen

Het gebruik van PHP-Scoper om een WordPress-plugin te scoppen is niet eenvoudig, en zelfs als het haalbaar is, is het gevoelig voor bugs.

Door de CMS-specifieke code en de bedrijfslogica van de applicatie volledig ontkoppeld te houden, kun je PHP-Scoper toepassen op slechts één set pakketten (die met de bedrijfslogica), en het vermijden op de andere (die met WordPress-code). Ik heb deze strategie gedetailleerd beschreven hier.

Bovendien, vergelijkbaar met PHP-Scoper, kunnen er andere tools zijn die falen wanneer ze worden toegepast op CMS-specifieke code (zoals WordPress). In die gevallen kan het granulaire opsplitsen van pakketten de redding zijn.

We kunnen verschillende applicaties maken, elk met alleen de code die ze nodig hebben

We kunnen onze pakketten hergebruiken om meer applicaties te maken, die alleen die pakketten bevatten die ze nodig hebben en niets anders.

Een persoonlijk blog heeft bijvoorbeeld misschien alleen posts, tags en categories nodig, waardoor het de functionaliteit voor users of user-login kan vermijden.

Sterker nog, ik plan binnenkort van deze functie te profiteren: ik werk momenteel aan de "Private GraphQL API", een op zichzelf staande GraphQL-engine, beschikbaar te maken voor WordPress-pluginontwikkelaars om in hun plugins in te bundelen, waarmee een GraphQL API voor hun Gutenberg-blokken wordt geboden.

Ik kan de "Private GraphQL API" moeiteloos maken door simpelweg de pakketten van de Gato GraphQL-plugin te verwijderen die niet nodig zijn (die voor UI, clients, custom endpoints, HTTP-caching, opgeslagen queries en een paar andere).

Ten slotte, omdat het eenvoudig te scoppen is (zoals hierboven te zien), kan ik alle vereiste pakketten van een prefix voorzien, zodat de Private GraphQL API zonder conflicten werkt (die zouden kunnen optreden wanneer 2 verschillende plugins verschillende versies van de Private GraphQL API inbundelen).

Nadelen van deze aanpak

Het spreekt voor zich dat deze aanpak verre van perfect is.

Meer inspanning, code wordt uitgebreider

Normaal gesproken voeren we, als onze applicatie op WordPress draait, gewoon get_posts uit om een lijst van berichten op te halen. Simpel en eenvoudig.

Het CMS-agnostic maken compliceert de zaken aanzienlijk. Om een lijst van berichten op te halen, moet je:

  • Pakketten posts en posts-wp aanmaken
  • Een contract met functie getPosts aanmaken in pakket posts
  • Het contract nakomen via get_posts in pakket posts-wp
  • Er altijd voor zorgen dat de functionaliteit via het contract wordt aangeroepen, nooit rechtstreeks

Het vereist (zeer waarschijnlijk) dependency injection

We moeten elk contract van het CMS-agnostic pakket koppelen aan zijn implementatie in het CMS-specifieke pakket. In mijn geval gebruik ik een service container, geleverd door de DependencyInjection-component van Symfony.

Ik ben dol op deze aanpak en ben van mening dat het de applicatie enorm vereenvoudigt. Ik begrijp echter dat niet elke applicatie anders dependency injection zou vereisen, wat er complexiteit aan toevoegt.

Het vereist (hoogstwaarschijnlijk) een monorepo

Gato GraphQL eindigde met 91 pakketten. Vroeger hostte ik elk pakket in zijn eigen repository, wat het maken van PR's erg moeilijk maakte. Ik ben dus "gedwongen" overgestapt op de monorepo-aanpak.

Om duidelijk te zijn: ik ben echt fan van de monorepo. Maar ik begrijp dat niet iedereen dat is, en het vereist ook zijn eigen inspanning om te onderhouden.

Ik heb eerder geschreven over mijn motivaties en strategie voor het abstraheren van mijn WordPress-website, waardoor het CMS-agnostic werd. Dit is dezelfde strategie die ik heb toegepast om de codebase van Gato GraphQL op te splitsen:

Addendum: Lijst van de 91 pakketten die de plugin vormen

Gato GraphQL bevat de volgende 91 pakketten.

Engine-functionaliteit:

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-functionaliteit:

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

GraphQL-serverfunctionaliteit:

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

Datamodel:

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

Abonneer je op onze nieuwsbrief

Blijf op de hoogte van alle updates over Gato GraphQL.