💁🏽♂️ Perché per supportare il CMS-agnosticism, Gato GraphQL è stato diviso in ~90 packages, e vantaggi e svantaggi di questo approccio
La settimana scorsa ho pubblicato l'articolo 💁🏻♀️ Perché Gato GraphQL ha bisogno di un monorepo, e come è ottimizzato, spiegando come e perché il monorepo GatoGraphQL/GatoGraphQL, che ospita il codice di Gato GraphQL, può gestire in modo efficiente il codebase del plugin.
Ho condiviso il mio articolo su Reddit, e ho ricevuto il seguente commento:
L'articolo dell'OP e gli articoli a cui rimanda si leggono come se un monorepo fosse la cosa migliore dai tempi del pane a fette.
Un articolo più interessante sarebbe spiegare perché hai pensato che il CMS-agnosticism richieda di dividere tutto nel proprio piccolo package, e perché hai pensato che ciascuno degli oltre 200 packages dovesse stare nel proprio repository fin dall'inizio.
È una domanda interessante. Ho quindi deciso di scrivere questo articolo per rispondere in modo un po' più approfondito.
Ma prima, affronterò due argomenti correlati: quanti packages sono effettivamente richiesti dal plugin, e perché affermo che il server GraphQL sottostante è CMS-agnostic.
Quanti packages compongono il plugin
Anche se ho menzionato oltre 200 packages PHP, questo vale per il monorepo; per il plugin, sono in realtà molti meno.
Il monorepo GatoGraphQL/GatoGraphQL comprende 5 progetti:
- PoP, una libreria di modelli di componenti lato server (come React, ma per il back-end)
- GraphQL by PoP, un server GraphQL CMS-agnostic per PHP
- Gato GraphQL
- un costruttore di siti (WIP)
- Wassup, un tema per siti web basato sul costruttore di siti (WIP)
Ospitare questi progetti in un monorepo semplifica il lavoro con essi, a causa delle loro interdipendenze:
- GraphQL by PoP è basato su PoP
- Gato GraphQL è basato su GraphQL by PoP
- Il costruttore di siti utilizza la libreria di modelli di componenti come suo motore (simile a Gatsby che usa GraphQL)
- Wassup è basato sul costruttore di siti
È riguardo al codice di tutti e 5 i progetti che GatoGraphQL/GatoGraphQL contiene oltre 200 packages PHP. Riguardo a Gato GraphQL, sono "solo" 91 packages. E GraphQL by PoP, il server GraphQL sottostante, contiene "solo" 98 packages.
(Il plugin Gato GraphQL richiede meno packages del suo server GraphQL sottostante, perché alcuni packages, come la direttiva @strTranslate di Google Translate, non sono ancora stati aggiunti al plugin.)
In che modo GraphQL by PoP è CMS-agnostic? In cosa è diverso da webonyx?
Ho detto che GraphQL by PoP è CMS-agnostic. Ma cosa significa?
Del resto, anche webonyx/graphql-php è CMS-agnostic. Allora in cosa sono diversi?
webonyx/graphql-php è CMS-agnostic, nel senso che è un package distribuito tramite Composer, contenente solo codice PHP "vanilla". Tuttavia, non è un server GraphQL di per sé; è piuttosto un'implementazione in PHP della specifica GraphQL, da integrare all'interno di un server GraphQL in PHP.
Ora, questi server GraphQL che la implementano, come Lighthouse o WPGraphQL, non sono CMS-agnostic. Non possiamo eseguire Lighthouse su WordPress, né WPGraphQL su Laravel.
È in questo senso che GraphQL by PoP è CMS-agnostic: è il server GraphQL "quasi-finale", quasi pronto per funzionare con qualsiasi CMS o framework, che sia Laravel, WordPress o qualunque altro. (Per brevità, d'ora in poi, ogni volta che dico "CMS", intendo "CMS o framework".)
Per renderlo finale per un determinato CMS, il server GraphQL avrà ancora bisogno di un po' di codice personalizzato per quel CMS, tramite un package corrispondente.
Affronterò ora le domande del commento.
Perché ogni package doveva stare nel proprio repository
Perché Packagist (il registro dei packages PHP di Composer) richiede di fornire un URL di repository per pubblicare/distribuire un package.
(A proposito, il mio articolo Hosting all your PHP packages together in a monorepo, anch'esso pubblicato la settimana scorsa, parla di questo problema.)
Perché il CMS-agnosticism richiede di dividere tutto nel proprio piccolo package
Ci sono alcune ragioni.
Far iniettare al CMS il proprio codice
È impossibile creare un server GraphQL che funzioni ovunque, utilizzando al 100% lo stesso codice PHP.
Ad esempio, per consentire a qualsiasi porzione di codice di modificare il valore di una variabile altrove, WordPress si affida ai filter hooks, Symfony usa il componente EventDispatcher, e Laravel ha il proprio sistema di eventi e listener. Il codice PHP per questi 3 diversi metodi sarà anch'esso diverso.
È qui che entra in gioco l'approccio di dividere il codice in packages granulari. Invece di avere una soluzione per gli eventi e i listener che faccia parte dell'applicazione, essa viene iniettata nell'applicazione tramite un package, e questo package conterrà codice specifico per il CMS.
Perché ciò funzioni, ogni funzionalità deve essere divisa in 2 packages:
- un package CMS-agnostic, contenente tutta la logica di business, utilizzando solo codice PHP "vanilla". Questo package includerà i contratti da soddisfare da parte del package specifico per il CMS
- un package specifico per il CMS, che soddisfa i contratti per quel CMS
Ad esempio, GraphQL by PoP ha un package hooks contenente il seguente contratto:
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;
}E poi, il package hooks-wp soddisfa il contratto per 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);
}
}Ora, anche se il concetto di hooks proviene da WordPress, può funzionare anche con altri CMS (ad esempio, utilizzando eventi e listener per implementare gli hooks). Possiamo quindi sostituire hooks-wp con hooks-laravel, hooks-symfony, hooks-drupal, hooks-octobercms, o qualunque altro, per soddisfare i contratti utilizzando il codice specifico per ciascun CMS.
Consentire al CMS di scartare le funzionalità che non può supportare
Non tutti i CMS possono supportare tutte le funzionalità. Ad esempio, WordPress consente di ordinare gli articoli per una voce meta_value, ma OctoberCMS non lo permette.
Ecco perché GraphQL by PoP contiene il package metaquery (soddisfatto per WordPress tramite metaquery-wp). Così, il server GraphQL implementato per WordPress includerà questo package, ma quello per OctoberCMS no.
Vantaggi di questo approccio
Dividere i nostri packages in modo granulare offre alcuni vantaggi.
Disaccoppiare la logica di business dal codice specifico per il CMS
Invece di codificare l'applicazione basandoci sull'opinionatedness (modo di codificare, funzionalità, limitazioni e altro) di un CMS, possiamo astrarre il nostro codice e usare solo la logica di business.
Ad esempio, per ottenere una lista di articoli, l'applicazione può eseguire il metodo getPosts da un'interfaccia in un package CMS-agnostic posts. Gli articoli verranno quindi sempre recuperati nello stesso modo, indipendentemente dall'implementazione del CMS sottostante.
Aggirare il debito tecnico e usare gli standard più recenti
Seguendo l'esempio precedente, recuperiamo i nostri articoli eseguendo il metodo getPosts, che segue la convenzione PSR-4, invece di chiamare get_posts, come definito da WordPress.
Allo stesso modo, possiamo eseguire getCustomPost per recuperare un custom post, invece dell'inaccurato get_post (questo fa parte del debito tecnico di WordPress).
È facile da scopare
Usare PHP-Scoper per scopare un plugin WordPress non è facile, e anche quando è fattibile, è soggetto a bug.
Mantenere il codice specifico per il CMS e la logica di business dell'applicazione ben disaccoppiati consente di applicare PHP-Scoper su un solo insieme di packages (quelli con la logica di business), ed evitarlo sugli altri (quelli contenenti codice WordPress). Ho descritto questa strategia in dettaglio, qui.
Inoltre, similmente a PHP-Scoper, potrebbero esserci altri strumenti che falliscono quando applicati a codice specifico per un CMS (come WordPress). In questi casi, dividere i packages in modo granulare può salvare la situazione.
Possiamo produrre applicazioni diverse, ciascuna contenente solo il codice di cui ha bisogno
Possiamo riutilizzare i nostri packages per produrre più applicazioni, contenenti solo i packages di cui hanno bisogno e nient'altro.
Ad esempio, un blog personale potrebbe aver bisogno solo di posts, tags e categories, quindi può evitare di occuparsi delle funzionalità per users o user-login.
In effetti, ho intenzione di sfruttare presto questa funzionalità: attualmente sto lavorando alla "Private GraphQL API", un motore GraphQL autonomo, da mettere a disposizione degli sviluppatori di plugin WordPress per integrarlo nei loro plugin, concedendo una API GraphQL per i loro blocchi Gutenberg.
Posso creare senza sforzo la "Private GraphQL API" semplicemente rimuovendo dal plugin Gato GraphQL i packages che non sono necessari (quelli che si occupano di UI, client, custom endpoint, cache HTTP, persisted queries e alcuni altri).
Infine, poiché è facile da scopare (come visto sopra), posso aggiungere un prefisso a tutti i packages richiesti, in modo che la Private GraphQL API funzioni senza conflitti (cosa che potrebbe accadere quando 2 plugin diversi integrano versioni diverse della Private GraphQL API).
Svantaggi di questo approccio
Inutile dire che questo approccio è tutt'altro che perfetto.
Maggiore sforzo, il codice diventa più verboso
Normalmente, se la nostra applicazione gira su WordPress, per recuperare una lista di articoli eseguiamo semplicemente get_posts. Semplice e facile.
Renderla CMS-agnostic complica notevolmente le cose. Per recuperare una lista di articoli, dobbiamo:
- Creare i packages
postseposts-wp - Creare un contratto con la funzione
getPostsnel packageposts - Soddisfare il contratto tramite
get_postsnel packageposts-wp - Assicurarci sempre di invocare la funzionalità tramite il contratto, mai direttamente
(Molto probabilmente) richiede l'iniezione di dipendenze
Dobbiamo legare ogni contratto del package CMS-agnostic, e la sua implementazione del package specifico per il CMS. Nel mio caso, sto usando un container di servizi, fornito dal componente DependencyInjection di Symfony.
Adoro questo approccio, credo che semplifichi notevolmente l'applicazione. Tuttavia, capisco che non tutte le applicazioni richiederebbero altrimenti l'iniezione di dipendenze, aggiungendovi complessità.
(Molto probabilmente) richiede un monorepo
Gato GraphQL è finito per contenere 91 packages. In passato, ospitavo ciascuno di essi nel proprio repository, il che rendeva molto difficile creare PR. Sono quindi stato "costretto" a passare all'approccio monorepo.
Per essere chiari: il monorepo mi piace davvero. Ma capisco che non piaccia a tutti, e richiede anch'esso il proprio sforzo di manutenzione.
Link utili
In precedenza ho scritto sulle mie motivazioni e sulla mia strategia per astrarre il mio sito WordPress, rendendolo CMS-agnostic. È questa stessa strategia che ho applicato per dividere il codebase di Gato GraphQL:
- Abstracting WordPress Code To Reuse With Other CMSs: Concepts (Part 1)
- Abstracting WordPress Code To Reuse With Other CMSs: Implementation (Part 2)
Appendice: Lista dei 91 packages che compongono il plugin
Gato GraphQL contiene i seguenti 91 packages.
Funzionalità del motore:
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
Funzionalità API:
getpop/api
getpop/api-clients
getpop/api-endpoints
getpop/api-endpoints-for-wp
getpop/api-graphql
getpop/api-mirrorquery
Funzionalità del server 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
Modello di dati:
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