🍾 Gato GraphQL est maintenant scopé, grâce à PHP-Scoper !
Le plugin Gato GraphQL est maintenant scopé. Cela signifie que le plugin peut enfin être téléversé dans le répertoire de plugins WordPress.

Pour y parvenir, j'utilise le merveilleux PHP-Scoper. Utiliser cette bibliothèque avec WordPress n'est pas sans défis, alors je vais expliquer dans cet article de blog comment j'ai réussi à m'en sortir.
Sections :
- Prendre la décision de scoper
- Explorer les options
- Essayer Mozart, et échouer
- Découvrir PHP-Scoper, et sortir en panique
- Revenir à PHP-Scoper, cette fois pour de bon
- PHP-Scoper, la méthode facile 😎 👈🏽 Ici commence ma solution
- Montre-moi les vraies choses
- Tests
- Voir les résultats
Prendre la décision de scoper
Il y a quelques semaines, Matt Mullenweg a annoncé qu'il allait garder un œil sur « le plugin GraphQL », faisant évidemment référence à WPGraphQL. Son expression démontre qu'il croit qu'il n'existe qu'un seul plugin GraphQL, alors qu'en réalité il y en a deux (celui qui est laissé de côté est, eh bien, le mien). Cela m'a fait réaliser à quel point mon plugin avait peu de visibilité, et je m'en suis senti mal.
Matt ne savait pas que mon plugin existait. La plupart de la communauté WordPress non plus, d'ailleurs. Il est clair que je ne le publicise pas assez bien. Je sais que je suis nul en marketing et réseaux sociaux ; je me débrouille seulement en matière technique (du moins, je le crois). J'ai donc décidé de faire quelque chose à ce sujet, au moins dans la limite de mes capacités.
Voici ce sur quoi je travaille :
- Je viens de finir de coder ce même site web, gatographql.com, et je l'ai lancé il y a 2 semaines (yay ! 🥳 Au fait, qu'en pensez-vous ? N'hésitez pas à me donner votre avis, via DM ou email)
- Il y a 3 jours, j'ai enfin commencé à scoper le plugin, et j'ai terminé cette tâche hier ! (À 3h du matin, mais ça valait le coup 😅)
- Et enfin, je travaille déjà sur la prochaine version
0.8, qui sera la première disponible dans le répertoire de plugins
Scoper le plugin est obligatoire pour le téléverser dans le répertoire, car sinon il pourrait entrer en conflit avec un autre plugin qui requiert la même dépendance que mon plugin, mais avec une version différente. L'avoir fait est une étape vraiment importante ; aucun autre développement n'est aussi important. Par exemple, je dois encore compléter le schéma GraphQL pour qu'il corresponde pleinement au modèle de données WordPress, mais cela sera fait de manière régulière à chaque nouvelle version.
Ainsi, dans quelques semaines, le plugin apparaîtra lors d'une recherche de « GraphQL », et les personnes qui ont réellement besoin d'implémenter une API GraphQL connaîtront l'existence de mon plugin.
En effet, je veux que mon plugin soit sérieusement considéré pour l'avenir de WordPress. Je travaille dessus depuis plusieurs années. Le dépôt a été créé en août 2016 ; c'est même avant que WPGraphQL existe, et au début de GraphQL. Mais je ne savais pas que le projet deviendrait un serveur GraphQL ; il a pris cette direction seulement il y a environ 1,5 an.
(Le projet est en réalité un framework pour construire des applications en utilisant des composants côté serveur, et un serveur GraphQL pourrait parfaitement être construit en utilisant cette architecture. J'ai donc simplement le construit.)
WPGraphQL est un plugin établi, et à juste titre : il a été lancé il y a quelques années, et une communauté s'est construite autour de lui. Le travail de Jason Bahl (qui est employé par Gatsby) et des contributeurs à son projet a été remarquable : intégrer WordPress dans le Jamstack est maintenant plus facile que jamais.
Mais une chose est Gatsby et le Jamstack, et une autre chose est WordPress. WordPress représente 40 % du web, pas seulement une entrée pour un générateur de sites statiques.
Maintenant, nous pouvons donc nous demander si WPGraphQL est la bonne option, sans que cette décision soit prise pour nous par manque d'alternatives. Nous pouvons maintenant analyser les deux plugins pour voir dont les objectifs sont le mieux alignés avec ce qui est important pour WordPress.
Gato GraphQL peut aussi fonctionner avec le Jamstack. Mais ses principaux objectifs sont, je crois, plus splendides : « démocratiser la publication de données », pour que modifier une API devienne aussi facile que modifier un article (quelque chose que tout le monde peut faire), et faire de WordPress le système d'exploitation du web.
Une fois que le plugin sera disponible dans le répertoire, j'espère que plus de personnes l'essaieront et diront « Hé, c'est franchement incroyable ! Comment se fait-il que je ne connaissais pas ça avant ? ».
Et alors, le choix du « plugin GraphQL » ne sera pas prédéterminé, et la communauté WordPress pourra considérer à la fois WPGraphQL et Gato GraphQL en fonction de leurs propres mérites.
Maintenant que mes motivations sont exposées, parlons des aspects techniques 🤓.
Explorer les options
Scoper un plugin implique d'exécuter certains outils qui prennent le code du plugin en entrée et produisent le plugin scopé. Pas de quoi s'inquiéter, non ? Ça peut être si difficile que ça ?

Eh bien, selon la base de code, exécuter la commande de scope seule ne suffira pas. Après cela, nous devons vérifier les erreurs dans la console, les corriger, tester l'application de manière approfondie, identifier les erreurs et comprendre pourquoi elles se produisent, les corriger, et itérer. Pour que tout soit parfait, cela peut prendre un certain temps.
Il existe 2 bibliothèques pour le scoping, avec des objectifs différents :
- Mozart, pour le code WordPress
- PHP-Scoper, pour tout code PHP, particulièrement lors de la production de PHARs
Comme j'ai un plugin WordPress, j'ai d'abord essayé Mozart. Voyons comment ça s'est passé.
Essayer Mozart, et échouer
J'ai essayé Mozart il y a environ 1 an. D'après ce que dit la documentation, « la commande mozart compose fait toute la magie ». Je m'attendais donc à ce que tout soit très rapide et simple, et à pouvoir profiter d'un daiquiri pour le reste de la journée.
Hélas, Mozart n'a jamais fonctionné pour ma base de code. Il continuait à rencontrer des problèmes, donc le scoping ne s'est jamais matérialisé. Et je n'ai pas pu obtenir l'aide nécessaire : j'ai soumis une PR, mais elle n'a pas été considérée pour fusion, et je n'en ai même pas été notifié, alors j'ai continué à attendre jusqu'à ce que je perde naturellement tout intérêt pour ce projet.
Je crois que Mozart ne pouvait pas gérer certaines des dépendances de mon plugin. J'utilise plusieurs composants Symfony, dont DependencyInjection, Cache et Dotenv, avec tout géré via Composer.
Scoper du PHP ne concerne pas seulement PHP, donc le scoper aura de nombreux obstacles à éviter et des défis à résoudre. Par exemple, Symfony DependencyInjection utilise des fichiers YAML pour la configuration, et ceux-ci doivent aussi être scopés. Et le fichier composer.json contient la configuration pour l'autoloading PSR-4, et cela doit aussi être scopé. Et, je crois, Mozart ne pouvait pas gérer correctement ces complexités.
Mais je suis sûr que mon expérience n'est pas la seule, et qu'il y a beaucoup d'utilisateurs heureux par là. De plus, ma tentative ratée s'est produite il y a 1 an, alors je me demande si l'outil a été amélioré depuis. Et puis, n'oublions pas le dicton : « Tous les plugins scopés se ressemblent ; chaque plugin non scopé l'est à sa façon », donc peut-être que ça échoue juste pour moi.
Si votre plugin WordPress est simple, avec une logique autonome, et que le scoping doit être effectué uniquement dans le code PHP, alors il y a de bonnes chances que Mozart fonctionne. Il faut juste le découvrir.
Découvrir PHP-Scoper, et sortir en panique
Je me suis donc dirigé vers PHP-Scoper. Cependant, je n'ai même pas essayé de l'essayer, car j'en ai eu peur immédiatement.
Pour commencer, cet outil ne supporte pas naturellement WordPress. Et pour continuer, ils recommandent de jeter un œil à leur propre Makefile, qui ressemble à ceci :
# See https://tech.davis-hansson.com/p/make/
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
.DEFAULT_GOAL := help
PHPBIN=php
PHPNOGC=php -d zend.enable_gc=0
IS_PHP8=$(shell php -r "echo version_compare(PHP_VERSION, '8.0.0', '>=') ? 'true' : 'false';")
SRC_FILES=$(shell find bin/ src/ -type f)
.PHONY: help
help:
@echo "\033[33mUsage:\033[0m\n make TARGET\n\n\033[32m#\n# Commands\n#---------------------------------------------------------------------------\033[0m\n"
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' | awk 'BEGIN {FS = ":"}; {printf "\033[33m%s:\033[0m%s\n", $$1, $$2}'
#
# Build
#---------------------------------------------------------------------------
.PHONY: clean
clean: ## Clean all created artifacts
clean:
git clean --exclude=.idea/ -ffdx
update-root-version: ## Check the lastest GitHub release and update COMPOSER_ROOT_VERSION accordingly
update-root-version:
rm .composer-root-version || true
$(MAKE) .composer-root-versionEt 600 lignes de plus, toutes comme ça. On dirait une énigme. En pensant que je devais comprendre ce code juste pour scoper mon plugin, j'ai fui sans cérémonie.
(Eh bien, comprendre ce code est leur recommandation pour tester l'application scopée, mais ce n'est pas obligatoire. Nous pouvons aussi simplement exécuter la commande php-scoper add-prefix, la laisser faire toute la magie, et aller boire nos daiquiris.)
Revenir à PHP-Scoper, cette fois pour de bon
Alors, il y a 3 jours, j'ai pris la décision d'implémenter le scoping, d'une façon ou d'une autre. Je devais y arriver.
Je suis revenu à PHP-Scoper, pour l'essayer sérieusement. Je savais que WordPress pouvait être scopé avec lui après avoir lu PHP Scoper: How to Avoid Namespace Issues in your Composer Dependencies (par les brillantes personnes de Delicious Brains). C'était juste une question d'attitude et de persévérance.
J'ai exploré certaines des solutions existantes, notamment :
- Celle-ci par Lucas Bustamante
- Celle-ci par Yoast
- Celle-ci par Google Site Kit
- Celle-ci par Google Web Stories
Mais elles me semblent toutes insatisfaisantes : soit le code paraît hacky, soit fragile et susceptible de casser à un moment ou à un autre.
Par exemple, le plugin Google Web Stories scope le code, puis annule chacun des conflits :
return [
'patchers' => [
function ( $file_path, $prefix, $contents ) {
/*
* There is currently no easy way to simply whitelist all global WordPress functions.
*
* This list here is a manual attempt after scanning through the AMP plugin, which means
* it needs to be maintained and kept in sync with any changes to the dependency.
*
* As long as there's no built-in solution in PHP-Scoper for this, an alternative could be
* to generate a list based on php-stubs/wordpress-stubs. devowlio/wp-react-starter/ seems
* to be doing just this successfully.
*
* @see https://github.com/humbug/php-scoper/issues/303
* @see https://github.com/php-stubs/wordpress-stubs
* @see https://github.com/devowlio/wp-react-starter/
*/
$contents = str_replace( "\\$prefix\\_doing_it_wrong", '\\_doing_it_wrong', $contents );
$contents = str_replace( "\\$prefix\\__", '\\__', $contents );
$contents = str_replace( "\\$prefix\\esc_html_e", '\\esc_html_e', $contents );
$contents = str_replace( "\\$prefix\\esc_html", '\\esc_html', $contents );
$contents = str_replace( "\\$prefix\\esc_attr", '\\esc_attr', $contents );
$contents = str_replace( "\\$prefix\\esc_url", '\\esc_url', $contents );
$contents = str_replace( "\\$prefix\\do_action", '\\do_action', $contents );
// ...
}
]
]Je comprends pourquoi ils le font, mais je n'aime pas ça. Chaque fois qu'une nouvelle fonction WordPress est référencée, ils doivent s'assurer qu'elle est également ajoutée à cette liste. C'est trop manuel, trop fragile.
Alors voici mon défi : N'existe-t-il pas une façon plus simple de scoper un plugin, en s'appuyant sur du code que nous pouvons présenter à nos amis et collègues sans rougir ?
PHP-Scoper, la méthode facile 😎
Il s'est avéré que c'était plus facile que je ne le pensais ! En seulement quelques heures, tout fonctionnait.

Maintenant, quand je dis « facile » et « heures », je veux dire : tout a fonctionné immédiatement, mais seulement après avoir passé 2 mois à créer la bonne structure pour la base de code (je l'expliquerai mieux plus tard).
Mais l'important est : si vous avez la bonne configuration pour le projet, le scoper peut être accompli en un rien de temps.
Le problème avec le scoping du code WordPress est, eh bien, le code WordPress. Le problème est expliqué ici, mais il se résume au fait que toutes les fonctions et classes WordPress sont aussi mises sous namespace. Donc si nous référençons WP_Query ou appelons get_posts dans notre code, ceux-ci seront transformés en MyPrefixedNamespace\WP_Query et MyPrefixedNamespace\get_posts, produisant un échec épique à l'exécution. Et cela ne peut être évité dans PHP-Scoper sans hacks.
Alors, quelle est la solution à cela ? Très simple : ne pas référencer WP_Query, ne pas appeler get_posts, et ne pas utiliser de code WordPress dans la base de code qui sera scopée.

Non, je ne suis pas fou, et je suis sûr que vous non plus. Et oui, je sais que nous construisons un plugin WordPress... Laissez-moi expliquer.
Comment peut-on ne pas inclure de code WordPress ? En divisant la base de code en 2 ensembles de packages :
- Ceux contenant du code WordPress, sans référencer de code d'aucune bibliothèque externe
- Ceux contenant la logique métier, sans contenir aucun code WordPress, et incluant toutes les dépendances requises et les références à leur code
De cette façon, au lieu d'avoir une seule base de code, nous avons plusieurs bases de code (ou packages), dont certains seront scopés et d'autres non, et ils forment tous ensemble le plugin, liés via Composer.
Ensuite, nous ne scopons pas le package contenant le code WordPress, évitant ainsi le conflit. Cela fonctionne car il ne référence aucun code appartenant à une dépendance externe. Toutes les références sont internes, comme MyNamespace\MyPlugin\MyClass. Mais celles-ci n'ont pas besoin d'être scopées, car nous pouvons supposer sans risque qu'il n'y aura qu'une seule version du plugin installée sur le site WordPress, et nous pouvons whitelister notre namespace MyNamespace\*.
De plus, si notre plugin peut être étendu, alors whitelister notre propre namespace est obligatoire. Par exemple, un field resolver pour Gato GraphQL est implémenté en étendant la classe PoP\ComponentModel\FieldResolvers\AbstractFieldResolver. Si je le scopais, les développeurs seraient forcés de référencer PoP\ComponentModel\FieldResolvers\AbstractFieldResolver pour le développement, et PrefixedByPoP\PoP\ComponentModel\FieldResolvers\AbstractFieldResolver pour la production. C'est inacceptable.
Ensuite, nous scopons uniquement les packages de logique métier, qui contiennent des références à toutes les bibliothèques externes mais aucun code WordPress.
En résumé, nous passons de cette stratégie :
« Avoir une seule base de code, la scoper, puis défaire douloureusement et avec beaucoup de patience les dégâts, tout en priant pour qu'aucun conflit ne passe inaperçu et 💣 n'explose en production »
À celle-ci :
« Diviser la base de code en 2 groupes, scoper seulement celui qui contient les références aux dépendances externes et aucun code WordPress, et aller prendre son daiquiri bien mérité 🍹 ».
Montre-moi les vraies choses
Il est temps d'ouvrir la saucisse et de voir si elle a de la vraie viande à l'intérieur 🌭.
Il y a 4 jours, j'avais le code suivant dans mon plugin :
namespace GraphQLAPI\GraphQLAPI\ContentProcessors;
use Parsedown;
class MarkdownContentParser
{
protected function getHTMLContent(string $fileContent): string
{
return (new Parsedown())->text($markdownContent);
}
}La classe Parsedown vient de la dépendance externe erusev/parsedown, comme défini dans le composer.json du plugin :
{
"require": {
"erusev/parsedown": "^1.7"
}
}Ainsi, mon plugin contenait des références à une bibliothèque externe, donc je devais le scoper, pour transformer Parsedown en PrefixedByPoP\Parsedown. Mais faire cela scoperait aussi tout le code WordPress du plugin, provoquant les conflits.
J'ai donc extrait le code dans un package séparé, appelé graphql-api/markdown-convertor, et remplacé la dépendance tierce dans composer.json par ma propre dépendance :
{
"require": {
"graphql-api/markdown-convertor": "^0.8"
}
}Maintenant, le plugin évite de référencer la bibliothèque externe ; à la place, il référence le service MarkdownConvertorInterface du nouveau package :
namespace GraphQLAPI\GraphQLAPI\ContentProcessors;
use GraphQLAPI\MarkdownConvertor\MarkdownConvertorInterface;
class MarkdownContentParser extends AbstractContentParser
{
protected MarkdownConvertorInterface $markdownConvertorInterface;
function __construct(MarkdownConvertorInterface $markdownConvertorInterface)
{
$this->markdownConvertorInterface = $markdownConvertorInterface;
}
protected function getHTMLContent(string $fileContent): string
{
return $this->markdownConvertorInterface->convertMarkdownToHTML($fileContent);
}
}La référence à la dépendance tierce est faite dans le nouveau package :
namespace GraphQLAPI\MarkdownConvertor;
use Parsedown;
class MarkdownConvertor implements MarkdownConvertorInterface
{
public function convertMarkdownToHTML(string $markdownContent): string
{
return (new Parsedown())->text($markdownContent);
}
}Enfin, nous devons :
- Scoper la dépendance
graphql-api/markdown-convertor - Ignorer le scoping du code du plugin
- Whitelister le namespace
GraphQLAPI\*, pour éviter que mes propres classes soient scopées
C'est pratiquement la stratégie. À partir de maintenant, ce sera une répétition de cette même idée, pour supprimer toutes les dépendances externes du code, jusqu'à ce que, voilà, le plugin puisse être scopé.
Les dépendances à extraire sont seulement celles de la section require de votre fichier composer.json ; pour require-dev vous pouvez garder n'importe quelle dépendance, externe ou non, car nous n'avons pas besoin de scoper les dépendances utilisées pour le développement ; seules celles pour créer et livrer le plugin, pour la production, doivent être scopées.
À la fin, le composer.json de votre plugin ne devrait contenir aucune dépendance externe. Pour mon plugin, il ressemble à ceci :
{
"require": {
"php": "^7.4|^8.0",
"getpop/engine-wp": "^0.8",
"graphql-api/markdown-convertor": "^0.8",
"graphql-by-pop/graphql-clients-for-wp": "^0.8",
"graphql-by-pop/graphql-endpoint-for-wp": "^0.8",
"graphql-by-pop/graphql-server": "^0.8",
"pop-schema/basic-directives": "^0.8",
"pop-schema/comment-mutations-wp": "^0.8",
"pop-schema/commentmeta-wp": "^0.8",
"pop-schema/comments-wp": "^0.8",
"pop-schema/custompost-mutations-wp": "^0.8",
"pop-schema/custompostmedia-mutations-wp": "^0.8",
"pop-schema/custompostmedia-wp": "^0.8",
"pop-schema/custompostmeta-wp": "^0.8",
"pop-schema/generic-customposts": "^0.8",
"pop-schema/media-wp": "^0.8",
"pop-schema/pages-wp": "^0.8",
"pop-schema/post-mutations": "^0.8",
"pop-schema/post-tags-wp": "^0.8",
"pop-schema/posts-wp": "^0.8",
"pop-schema/taxonomymeta-wp": "^0.8",
"pop-schema/taxonomyquery-wp": "^0.8",
"pop-schema/user-roles-access-control": "^0.8",
"pop-schema/user-roles-wp": "^0.8",
"pop-schema/user-state-mutations-wp": "^0.8",
"pop-schema/user-state-wp": "^0.8",
"pop-schema/usermeta-wp": "^0.8",
"pop-schema/users-wp": "^0.8"
}
}Tous ces packages, avec les namespaces getpop, graphql-api, graphql-by-pop, et pop-schema, sont les miens : des dépendances contenant tout le code du plugin. Ils sont distribués dans différents namespaces pour mieux gérer le code, mais vous n'en avez pas besoin : utiliser un seul namespace fonctionne bien.
Maintenant, à mesure que le nombre de packages dans votre application augmente, vous devrez les héberger tous dans un monorepo, ou vous deviendrez fou en créant des pull requests impliquant plus d'un package (croyez-moi, j'y suis passé). Dans mon cas, tous mes packages sont hébergés dans le monorepo GatoGraphQL/GatoGraphQL, et je les garde synchronisés via le merveilleux Monorepo Builder (je dois écrire un article sur cet outil, c'est un vrai sauveur !).
Les namespaces pour ces packages sont PoP, GraphQLAPI, GraphQLByPoP et PoPSchema. Comme ils sont les miens, je sais qu'ils n'apparaîtront qu'une seule fois dans l'application, et je peux donc éviter de les scoper.
Pour ce faire, je les ajoute à la whitelist dans scoper.inc.php :
return [
'whitelist' => [
// Own namespaces
'PoPSchema\*',
'PoP\*',
'GraphQLByPoP\*',
'GraphQLAPI\*',
// Own container cache
'PoPContainer\*',
],
];La dernière entrée correspond au conteneur d'injection de dépendances, qui doit aussi être scopé. Par défaut, ce conteneur se voit attribuer le nom ProjectServiceContainer, directement dans le namespace global. Mais PHP-Scoper ne supporte pas le whitelisting de classes spécifiques du namespace global. J'ai donc ajouté le namespace artificiel PoPContainer à la whitelist, et assigné ce namespace lors du dump du conteneur sur le disque :
$dumper = new PhpDumper($containerBuilder);
file_put_contents(
self::$cacheFile,
$dumper->dump(
// Save under own namespace to avoid conflicts
array('namespace' => 'PoPContainer')
)
);Vous pouvez remarquer que, concernant les packages, certains se terminent par -wp (comme pop-schema/users-wp) tandis que d'autres non (comme graphql-by-pop/graphql-server). Oui, vous avez deviné : les premiers contiennent du code WordPress et aucune référence à des bibliothèques externes, et les seconds peuvent contenir des références à des bibliothèques externes, mais aucun code WordPress.
Ensuite, je saute le scoping des packages WordPress :
return [
'finders' => [
// Scope packages under vendor/, excluding local WordPress packages
Finder::create()
->files()
->notPath([
// Exclude libraries ending in "-wp"
'#getpop/[a-zA-Z0-9_-]*-wp/#',
'#pop-schema/[a-zA-Z0-9_-]*-wp/#',
'#graphql-by-pop/[a-zA-Z0-9_-]*-wp/#',
])
->in('vendor')
]
];Que se passe-t-il si un package WordPress doit référencer une bibliothèque externe, et que cela ne peut pas être extrait dans un autre package ? Par exemple, mon package getpop/routing-wp dépend de brain/cortex, et c'est inévitable.
Je ne peux pas scoper l'ensemble du package, car getpop/routing-wp contient du code WordPress. À la place, je dois identifier les fichiers où ces références sont faites, et m'assurer qu'ils ne contiennent pas de code WordPress. Je peux alors scoper seulement ces fichiers.
Dans ce cas, la référence à Cortex/Brain est faite dans 2 fichiers, dont layers/Engine/packages/routing-wp/src/Hooks/SetupCortexHookSet.php :
namespace PoP\RoutingWP\Hooks;
use PoP\Hooks\AbstractHookSet;
use Brain\Cortex\Route\RouteCollectionInterface;
use Brain\Cortex\Route\RouteInterface;
use Brain\Cortex\Route\QueryRoute;
use PoP\RoutingWP\WPQueries;
use PoP\Routing\Facades\RoutingManagerFacade;
class SetupCortexHookSet extends AbstractHookSet
{
protected function init()
{
$this->hooksAPI->addAction(
'cortex.routes',
[$this, 'setupCortex'],
1
);
}
/**
* @param RouteCollectionInterface<RouteInterface> $routes
*/
public function setupCortex(RouteCollectionInterface $routes): void
{
$routingManager = RoutingManagerFacade::getInstance();
foreach ($routingManager->getRoutes() as $route) {
$routes->addRoute(new QueryRoute(
$route,
function (array $matches) {
return WPQueries::STANDARD_NATURE;
}
));
}
}
}Remarquez-vous l'anomalie ici ? C'est une implémentation d'un hook, mais aucun add_action n'est appelé, car je ne peux pas avoir de code WordPress ici. À la place, il appelle la fonction addAction du service HooksAPIInterface, et ce service est implémenté par la classe HooksAPI dans le package getpop/hooks-wp, où nous pouvons avoir du code WordPress :
namespace PoP\HooksWP;
use PoP\Hooks\HooksAPIInterface;
class HooksAPI implements HooksAPIInterface
{
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);
}
}Maintenant que le code est proprement divisé, nous pouvons scoper ces 2 fichiers référençant des dépendances externes :
return [
'finders' => [
Finder::create()->append([
'vendor/getpop/routing-wp/src/Component.php',
'vendor/getpop/routing-wp/src/Hooks/SetupCortexHookSet.php',
])
]
];J'ai mentionné plus tôt que la configuration du scoping a pris quelques heures, mais seulement après 2 mois de travail. Eh bien, cet exemple démontre ce que je voulais dire : le vrai travail réside dans la division propre de la base de code en 2 ensembles.
Dans mon cas, le travail a pris 2 mois car le niveau de détail était extrême : le plugin est devenu une composition de 125 packages ! Mais c'est un cas exceptionnel, avec l'objectif que le serveur sous-jacent du plugin soit CMS-agnostic, afin de supporter une implémentation pour d'autres CMSs/frameworks en réimplémentant simplement les packages -wp correspondants.
(J'ai écrit en détail sur cette stratégie, dans les articles Abstracting WordPress Code To Reuse With Other CMSs: Concepts et Implementation.)
C'est certes beaucoup de travail, mais la propreté améliorée du code en vaut la peine. Et pas seulement pour scoper le plugin, ce qui m'est venu comme une totale surprise, et je suis encore content de cette heureuse découverte inattendue. Par exemple, j'exécute PHPStan et PHPUnit séparément sur le code WordPress et non-WordPress, m'évitant ainsi de nombreux maux de tête.
Une fois la base de code bien rangée, le monde devient soudainement un endroit bien meilleur.
Tests
Alors, comment tester cette bête ?
La solution que j'ai trouvée est de m'appuyer sur Rector, le même outil que j'utilise pour downgrader le code de PHP 7.4, pour le développement, à 7.1, pour la production.
L'idée est la suivante :
- Scoper le plugin
- L'analyser avec Rector, en appliquant n'importe quelle règle (peu importe laquelle)
Si quelque chose s'est mal passé lors du scoping, alors Rector ne pourra pas charger certaines classes, et il lancera une erreur. Par exemple, si la classe Brain\Cortex a été scopée comme PrefixedByPoP\Brain\Cortex, mais qu'une référence à elle a été laissée comme Brain\Cortex, alors l'autoloading de cette classe échouera.
Voici ma GitHub Action pour les tests (working-directory est utilisé, car j'opère depuis la racine du monorepo, mais le scoping se produit dans le dossier du plugin) :
name: Scope Gato GraphQL tests
on:
push:
branches:
- master
pull_request: null
env:
COMPOSER_ROOT_VERSION: "dev-master"
jobs:
main:
defaults:
run:
working-directory: layers/GraphQLAPIForWP/plugins/graphql-api-for-wp
name: Scope the plugin code via PHP-Scoper, and execute tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set-up PHP
uses: shivammathur/setup-php@v2
with:
php-version: 7.4
coverage: none
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install root dependencies
uses: "ramsey/composer-install@v1"
- name: Install plugin dependencies for PROD
run: composer install --no-dev --no-progress --no-interaction --ansi
- name: Install PHP-Scoper
run: |
composer global config minimum-stability dev
composer global config prefer-stable true
composer global require humbug/php-scoper
# The scoped results correspond to vendor/, so must generate them in such folder
- name: Scope plugin into separate folder
run: php-scoper add-prefix --output-dir ../../../../build-prefixed/vendor --ansi
- name: Copy scoped code back into plugin
run: rsync -av build-prefixed/ layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/ --quiet
working-directory: .
- name: Regenerate autoloader
run: composer dumpautoload --optimize --classmap-authoritative --ansi
- name: Run Rector on the scoped code
run: vendor/bin/rector process --config=layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/rector-test-scoping.php --ansi
working-directory: .
Et voici ma configuration Rector :
use Rector\CodeQuality\Rector\LogicalAnd\AndAssignsToSeparateLinesRector;
use Rector\Core\Configuration\Option;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(AndAssignsToSeparateLinesRector::class);
$parameters->set(Option::AUTO_IMPORT_NAMES, true);
$parameters->set(Option::AUTOLOAD_PATHS, [
__DIR__ . '/vendor/scoper-autoload.php',
__DIR__ . '/vendor/erusev/parsedown/Parsedown.php',
__DIR__ . '/vendor/jrfnl/php-cast-to-type/cast-to-type.php',
__DIR__ . '/vendor/jrfnl/php-cast-to-type/class.cast-to-type.php',
]);
// files to rector
$parameters->set(Option::PATHS, [
__DIR__ . '/vendor',
]);
// files to skip
$parameters->set(Option::SKIP, [
// Exclude tests
'*/tests/*',
__DIR__ . '/vendor/nikic/fast-route/test/*',
__DIR__ . '/vendor/psr/log/Psr/Log/Test/*',
__DIR__ . '/vendor/symfony/service-contracts/Test/*',
]);
};Vous pouvez remarquer que certains fichiers de dépendances, comme erusev/parsedown/Parsedown.php' doivent être ajoutés à Option::AUTOLOAD_PATHS. C'est parce que scoper le composer.json du package n'est pas fiable à 100 %, et alors leur autoloading peut échouer.
Quand cela se produit, Rector se plaindra qu'une certaine classe a échoué à l'autoloading. À partir de là, nous identifions le fichier correspondant, et nous l'ajoutons manuellement aux chemins d'autoloading.
Voir les résultats
Voici le code source du plugin, et voici sa version scopée (et downgraded vers PHP 7.1).
Trouvez les 7 différences 😁. (Je vous donne un indice : cherchez PrefixedByPoP.)
Et voici le fichier plugin final graphql-api.zip, prêt à être installé sur votre site.
C'est tout. J'espère que cela a été utile 😃💪🚀