Blog

đŸ’đŸœâ€â™‚ïž Pourquoi pour soutenir le CMS-agnosticism, Gato GraphQL a Ă©tĂ© divisĂ© en ~90 packages, et avantages et inconvĂ©nients de cette approche

Leonardo Losoviz
Par Leonardo Losoviz ·

La semaine derniĂšre, j'ai publiĂ© l'article đŸ’đŸ»â€â™€ïž Pourquoi Gato GraphQL a besoin d'un monorepo, et comment il est optimisĂ©, expliquant comment et pourquoi le monorepo GatoGraphQL/GatoGraphQL, qui hĂ©berge le code de Gato GraphQL, peut gĂ©rer efficacement la base de code du plugin.

J'ai partagé mon article sur Reddit, et j'ai reçu le commentaire suivant :

L'article de l'OP et les articles auxquels il renvoie se lisent comme si un monorepo était la meilleure chose depuis le pain en tranches.

Un article plus intĂ©ressant serait d'expliquer pourquoi vous avez pensĂ© que le CMS-agnosticism nĂ©cessite de tout diviser en son propre petit package, et pourquoi vous avez pensĂ© que chacun des plus de 200 packages devait ĂȘtre dans son propre dĂ©pĂŽt au dĂ©part.

C'est une question intéressante. J'ai donc décidé d'écrire cet article pour y répondre un peu plus en détail.

Mais d'abord, j'aborderai deux sujets connexes : combien de packages sont réellement requis par le plugin, et pourquoi j'affirme que le serveur GraphQL sous-jacent est CMS-agnostic.

Combien de packages composent le plugin

MĂȘme si j'ai mentionnĂ© plus de 200 packages PHP, c'est pour le monorepo ; pour le plugin, c'est en rĂ©alitĂ© bien moins que ça.

Le monorepo GatoGraphQL/GatoGraphQL englobe 5 projets :

  1. PoP, une bibliothÚque de modÚles de composants cÎté serveur (comme React, mais pour le back-end)
  2. GraphQL by PoP, un serveur GraphQL CMS-agnostic pour PHP
  3. Gato GraphQL
  4. un constructeur de site (WIP)
  5. Wassup, un thÚme de site web basé sur le constructeur de site (WIP)

Héberger ces projets dans un monorepo simplifie le travail avec eux, en raison de leurs interdépendances :

  • GraphQL by PoP est basĂ© sur PoP
  • Gato GraphQL est basĂ© sur GraphQL by PoP
  • Le constructeur de site utilise la bibliothĂšque de modĂšles de composants comme moteur (similaire Ă  Gatsby utilisant GraphQL)
  • Wassup est basĂ© sur le constructeur de site

C'est concernant le code des 5 projets que GatoGraphQL/GatoGraphQL contient plus de 200 packages PHP. Concernant Gato GraphQL, ce sont « seulement » 91 packages. Et GraphQL by PoP, le serveur GraphQL sous-jacent, contient « seulement » 98 packages.

(Le plugin Gato GraphQL nécessite moins de packages que son serveur GraphQL sous-jacent, car certains packages, comme la directive @strTranslate de Google Translate, n'ont pas encore été ajoutés au plugin.)

Comment GraphQL by PoP est-il CMS-agnostic ? En quoi est-il différent de webonyx ?

J'ai dit que GraphQL by PoP est CMS-agnostic. Mais qu'est-ce que cela signifie ?

À ce titre, webonyx/graphql-php est Ă©galement CMS-agnostic. Alors en quoi sont-ils diffĂ©rents ?

webonyx/graphql-php est CMS-agnostic, en ce sens qu'il s'agit d'un package distribué via Composer, contenant uniquement du code PHP « vanilla ». Cependant, ce n'est pas un serveur GraphQL en soi ; c'est plutÎt une implémentation en PHP de la spécification GraphQL, à intégrer dans un serveur GraphQL en PHP.

Or, ces serveurs GraphQL qui l'implémentent, comme Lighthouse ou WPGraphQL, ne sont pas CMS-agnostic. On ne peut pas exécuter Lighthouse sur WordPress, ni WPGraphQL sur Laravel.

C'est en ce sens que GraphQL by PoP est CMS-agnostic : c'est le serveur GraphQL « presque-final », presque prĂȘt Ă  fonctionner avec n'importe quel CMS ou framework, que ce soit Laravel, WordPress ou tout autre. (Par souci de briĂšvetĂ©, dĂ©sormais, quand je dis « CMS », cela signifie « CMS ou framework ».)

Pour le rendre final pour un CMS donné, le serveur GraphQL aura encore besoin d'un peu de code personnalisé pour ce CMS, via un package correspondant.

J'aborderai maintenant les questions du commentaire.

Pourquoi chaque package devait ĂȘtre dans son propre dĂ©pĂŽt

Parce que Packagist (le registre des packages PHP de Composer) exige de fournir une URL de dépÎt pour publier/distribuer un package.

(Au fait, mon article Hosting all your PHP packages together in a monorepo, également publié la semaine derniÚre, parle de ce problÚme.)

Pourquoi le CMS-agnosticism exige de tout diviser en son propre petit package

Il y a quelques raisons.

Permettre au CMS d'injecter son propre code

Il est impossible de crĂ©er un serveur GraphQL qui fonctionne partout, en utilisant 100 % du mĂȘme code PHP.

Par exemple, pour permettre à n'importe quel morceau de code de modifier la valeur d'une variable ailleurs, WordPress s'appuie sur les filter hooks, Symfony utilise le composant EventDispatcher, et Laravel a son propre systÚme d'événements et listeners. Le code PHP pour ces 3 méthodes différentes sera également différent.

C'est là qu'intervient l'approche consistant à diviser le code en packages granulaires. Au lieu d'avoir une solution pour les événements et les listeners qui fait partie de l'application, elle est injectée dans l'application via un package, et ce package contiendra du code spécifique au CMS.

Pour que cela fonctionne, chaque fonctionnalitĂ© doit ĂȘtre divisĂ©e en 2 packages :

  • un package CMS-agnostic, contenant toute la logique mĂ©tier, n'utilisant que du code PHP « vanilla ». Ce package inclura les contrats Ă  satisfaire par le package spĂ©cifique au CMS
  • un package spĂ©cifique au CMS, satisfaisant les contrats pour ce CMS

Par exemple, GraphQL by PoP a un package hooks contenant le contrat suivant :

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

Et ensuite, le package hooks-wp satisfait le contrat pour 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);
  }
}

Maintenant, mĂȘme si le concept de hooks vient de WordPress, il peut Ă©galement fonctionner avec d'autres CMS (par exemple, en utilisant des Ă©vĂ©nements et des listeners pour implĂ©menter les hooks). On peut alors remplacer hooks-wp par hooks-laravel, hooks-symfony, hooks-drupal, hooks-octobercms, ou tout autre, pour satisfaire les contrats en utilisant le code spĂ©cifique Ă  chaque CMS.

Permettre au CMS d'écarter les fonctionnalités qu'il ne peut pas prendre en charge

Tous les CMS ne peuvent pas prendre en charge toutes les fonctionnalités. Par exemple, WordPress permet de trier les articles par une entrée meta_value, mais OctoberCMS ne le permet pas.

C'est pourquoi GraphQL by PoP contient le package metaquery (satisfait pour WordPress via metaquery-wp). Ainsi, le serveur GraphQL implémenté pour WordPress inclura ce package, mais celui pour OctoberCMS ne le fera pas.

Avantages de cette approche

Diviser nos packages de maniĂšre granulaire offre quelques avantages.

Découpler la logique métier du code spécifique au CMS

Au lieu de coder l'application en se basant sur l'opinionatedness (façon de coder, fonctionnalités, limitations, et autres) d'un CMS, nous pouvons abstraire notre code et n'utiliser que la logique métier.

Par exemple, pour obtenir une liste d'articles, l'application peut exĂ©cuter la mĂ©thode getPosts depuis une interface dans un package CMS-agnostic posts. Les articles seront alors toujours rĂ©cupĂ©rĂ©s de la mĂȘme maniĂšre, indĂ©pendamment de l'implĂ©mentation du CMS sous-jacent.

Contourner la dette technique et utiliser les derniĂšres normes

En suivant l'exemple ci-dessus, nous récupérons nos articles en exécutant la méthode getPosts, qui suit la convention PSR-4, au lieu d'appeler get_posts, comme défini par WordPress.

De mĂȘme, nous pouvons exĂ©cuter getCustomPost pour rĂ©cupĂ©rer un custom post, au lieu de l'inexact get_post (cela fait partie de la dette technique de WordPress).

C'est facile Ă  scoper

Utiliser PHP-Scoper pour scoper un plugin WordPress n'est pas facile, et mĂȘme quand c'est faisable, c'est sujet Ă  des bugs.

Garder le code spécifique au CMS et la logique métier de l'application bien découplés permet d'appliquer PHP-Scoper sur un seul ensemble de packages (ceux avec la logique métier), et de l'éviter sur les autres (ceux contenant du code WordPress). J'ai décrit cette stratégie en détail, ici.

De plus, similairement à PHP-Scoper, il peut y avoir d'autres outils qui échouent lorsqu'ils sont appliqués à du code spécifique à un CMS (comme WordPress). Dans ces cas, diviser les packages de maniÚre granulaire peut sauver la mise.

Nous pouvons produire différentes applications, chacune ne contenant que le code dont elle a besoin

Nous pouvons réutiliser nos packages pour produire davantage d'applications, ne contenant que les packages dont elles ont besoin et rien d'autre.

Par exemple, un blog personnel peut n'avoir besoin que de posts, tags et categories, donc il peut éviter de traiter avec des fonctionnalités pour users ou user-login.

En effet, je prévois de bénéficier prochainement de cette fonctionnalité : je travaille actuellement sur la « Private GraphQL API », un moteur GraphQL autonome, à mettre à disposition des développeurs de plugins WordPress pour l'intégrer dans leurs plugins, accordant une API GraphQL pour leurs blocs Gutenberg.

Je peux créer sans effort la « Private GraphQL API » en supprimant simplement les packages du plugin Gato GraphQL qui ne sont pas nécessaires (ceux qui traitent de l'interface utilisateur, des clients, des custom endpoints, du cache HTTP, des persisted queries, et quelques autres).

Enfin, comme il est facile de scoper (comme vu ci-dessus), je peux préfixer tous les packages requis, de sorte que la Private GraphQL API fonctionnera sans conflit (ce qui pourrait arriver quand 2 plugins différents intÚgrent différentes versions de la Private GraphQL API).

Inconvénients de cette approche

Inutile de dire que cette approche est loin d'ĂȘtre parfaite.

Plus d'effort, le code devient plus verbeux

Normalement, si notre application fonctionne sur WordPress, pour récupérer une liste d'articles nous exécutons simplement get_posts. Simple et facile.

La rendre CMS-agnostic complique considérablement les choses. Pour récupérer une liste d'articles, nous devons :

  • CrĂ©er les packages posts et posts-wp
  • CrĂ©er un contrat avec la fonction getPosts dans le package posts
  • Satisfaire le contrat via get_posts dans le package posts-wp
  • Toujours s'assurer d'invoquer la fonctionnalitĂ© via le contrat, jamais directement

Cela nécessite (trÚs probablement) l'injection de dépendances

Nous devons lier chaque contrat du package CMS-agnostic, et son implémentation du package spécifique au CMS. Dans mon cas, j'utilise un conteneur de services, fourni par le composant DependencyInjection de Symfony.

J'adore cette approche, je crois qu'elle simplifie grandement l'application. Cependant, je comprends que toutes les applications n'exigeraient pas autrement l'injection de dépendances, ajoutant de la complexité à celle-ci.

Cela nécessite (trÚs probablement) un monorepo

Gato GraphQL s'est retrouvé avec 91 packages. Dans le passé, j'hébergeais chacun d'eux dans son propre dépÎt, ce qui rendait trÚs difficile la création de PRs. J'ai donc été « forcé » de passer à l'approche monorepo.

Pour ĂȘtre clair : j'adore vraiment le monorepo. Mais je comprends que tout le monde ne l'apprĂ©cie pas, et il nĂ©cessite Ă©galement ses propres efforts de maintenance.

Liens utiles

J'ai prĂ©cĂ©demment Ă©crit sur mes motivations et ma stratĂ©gie pour abstraire mon site WordPress, le rendant CMS-agnostic. C'est cette mĂȘme stratĂ©gie que j'ai appliquĂ©e pour diviser la base de code de Gato GraphQL :

Addendum : Liste des 91 packages composant le plugin

Gato GraphQL contient les 91 packages suivants.

Fonctionnalité du moteur :

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

Fonctionnalité API :

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

Fonctionnalité du serveur 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

ModÚle de données :

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

Abonnez-vous Ă  notre newsletter

Restez au courant de toutes les nouveautés de Gato GraphQL.