🍾 ¡Gato GraphQL ya está scoped, gracias a PHP-Scoper!
El plugin Gato GraphQL ya está scoped. Esto significa que el plugin por fin puede subirse al directorio de plugins de WordPress.

Para conseguirlo, estoy usando el maravilloso PHP-Scoper. Usar esta librería con WordPress no está exento de dificultades, así que en este artículo te explicaré cómo logré sacarlo adelante.
Secciones:
- Tomar la decisión de aplicar scoping
- Explorando las opciones
- Probando Mozart, y fracasando
- Echando un vistazo a PHP-Scoper, y saliendo aterrado
- Volviendo a PHP-Scoper, esta vez para quedarme
- PHP-Scoper, la forma fácil 😎 👈🏽 Aquí empieza mi solución
- Enséñame lo bueno de verdad
- Pruebas
- Mira los resultados
Tomar la decisión de aplicar scoping
Hace unas semanas, Matt Mullenweg anunció que estará pendiente del "plugin de GraphQL", refiriéndose obviamente a WPGraphQL. Su expresión demuestra que cree que solo existe un plugin de GraphQL, cuando en realidad hay dos (el que se queda fuera es, bueno, el mío). Eso me hizo darme cuenta de la poca visibilidad que tiene mi plugin, y me sentí mal por ello.
Matt no sabía que mi plugin existía. La mayoría de la comunidad WordPress tampoco, para el caso. Claramente no lo estoy promocionando lo suficiente. Sé que soy malo en marketing y redes sociales; solo se me dan bien las cosas técnicas (o eso creo). Así que decidí hacer algo al respecto, al menos dentro de mis posibilidades.
Esto es en lo que estoy trabajando:
- Acabo de terminar de codificar este mismo sitio web, gatographql.com, y lo lancé hace 2 semanas (¡yay! 🥳 Por cierto, ¿qué te parece? Eres bienvenido a darme feedback, vía DM o email)
- Hace 3 días, por fin empecé el scoping del plugin, ¡y ayer terminé esta tarea! (A las 3 a. m., pero mereció la pena 😅)
- Y por último, ya estoy trabajando en la próxima versión
0.8, que será la primera disponible en el repositorio de plugins
Hacer scoping del plugin es obligatorio para subirlo al repositorio, porque de lo contrario podría entrar en conflicto con otro plugin, que requiriera la misma dependencia que mi plugin, pero con una versión distinta. Haberlo conseguido es un hito enorme; ningún otro desarrollo es tan importante. Por ejemplo, todavía debo completar el esquema de GraphQL para que se ajuste plenamente al modelo de datos de WordPress, pero eso se irá haciendo de forma constante con cada nueva release.
Así que dentro de unas semanas, el plugin aparecerá al buscar "GraphQL", y las personas que realmente necesiten implementar una API de GraphQL conocerán la existencia de mi plugin.
Efectivamente, quiero que mi plugin se considere seriamente para el futuro de WordPress. Llevo varios años trabajando en él. El repo se creó en agosto de 2016; eso es incluso antes de que existiera WPGraphQL, y al principio de GraphQL. Pero no sabía que el proyecto se convertiría en un servidor de GraphQL; tomó esa dirección hace solo 1,5 años.
(El proyecto es en realidad un framework para construir aplicaciones usando componentes del lado del servidor, y un servidor de GraphQL podría construirse perfectamente con esta arquitectura. Así que simplemente lo construí).
WPGraphQL es un plugin consolidado, y con razón: se inició hace unos años, y se ha formado una comunidad a su alrededor. El trabajo de Jason Bahl (que está empleado por Gatsby) y los contribuidores a su proyecto ha sido sobresaliente: integrar WordPress en el Jamstack es ahora más fácil que nunca.
Pero una cosa es Gatsby y el Jamstack, y otra cosa es WordPress. WordPress es el 40 % de la web, no solo una entrada para un generador de sitios estáticos.
Así que ahora, podemos plantearnos si WPGraphQL es la opción adecuada, sin tener esta decisión tomada por nosotros por falta de alternativas. Ahora podemos analizar ambos plugins para ver cuáles objetivos están más alineados con lo que es importante para WordPress.
Gato GraphQL también puede funcionar con el Jamstack. Pero sus objetivos principales son, creo, más espléndidos: "democratizar la publicación de datos", para que editar una API sea tan fácil como editar una entrada (algo que todo el mundo puede hacer), y conseguir que WordPress se convierta en el SO de la web.
Una vez que el plugin esté disponible en el repositorio, espero que más gente lo pruebe y diga "¡Eh, esto es flipantemente increíble! ¿Cómo es que no conocía esto antes?".
Y entonces, la elección del "plugin de GraphQL" no está predeterminada, y la comunidad WordPress puede considerar tanto WPGraphQL como Gato GraphQL en base a sus propios méritos.
Ahora que mis motivaciones están aclaradas, hablemos de cosas técnicas 🤓.
Explorando las opciones
Hacer scoping de un plugin implica ejecutar algunas herramientas, que toman el código del plugin como entrada, y producen el plugin scoped. No es para tanto, ¿verdad? ¿Cómo de difícil puede ser?

Bueno, dependiendo del código base, ejecutar el comando de scope por sí solo no será suficiente. Después de eso, necesitamos revisar errores en la consola, arreglarlos, probar la aplicación a fondo, identificar errores y por qué ocurren, arreglarlos, e iterar. Para hacerlo completamente bien, puede requerir algo de tiempo.
Hay 2 librerías para scoping, con objetivos diferentes:
- Mozart, para código de WordPress
- PHP-Scoper, para cualquier código PHP, particularmente al producir PHARs
Como tengo un plugin de WordPress, primero probé Mozart. Veamos cómo me fue.
Probando Mozart, y fracasando
Probé Mozart hace alrededor de 1 año. Según dice la documentación, "el comando mozart compose hace toda la magia". Así que esperaba que todo fuera muy rápido y sencillo, y disfrutar de un daiquiri el resto del día.
Por desgracia, Mozart nunca funcionó para mi código base. Seguía topándose con problemas, por lo que el scoping nunca se materializó. Y no conseguí la ayuda necesaria: envié un PR, pero no se consideró para mergear, y ni siquiera me notificaron al respecto, así que seguí esperando hasta que naturalmente perdí el interés en este proyecto.
Creo que Mozart no podía manejar algunas de las dependencias en mi plugin. Estoy usando varios componentes de Symfony, incluyendo DependencyInjection, Cache y Dotenv, con todo gestionado a través de Composer.
Hacer scoping de PHP no es solo cosa de PHP, así que el scoper tendrá muchos obstáculos que evitar y retos que resolver. Por ejemplo, Symfony DependencyInjection usa ficheros YAML para configurar, y estos también deben ser scoped. Y el fichero composer.json contiene la configuración para el autoloading PSR-4, y esto también debe ser scoped. Y, creo, Mozart no podía manejar bien estas complejidades.
Pero seguro que mi experiencia no es la única, y que hay muchos usuarios felices por ahí. Además, mi intento fallido fue hace 1 año, así que me pregunto si la herramienta ha mejorado desde entonces. Y luego, no olvides el dicho: "Todos los plugins scoped se parecen; cada plugin no-scoped lo es a su manera", así que posiblemente falla solo para mí.
Si tu plugin de WordPress es sencillo, con lógica autocontenida, y el scoping debe realizarse únicamente dentro del código PHP, entonces es probable que Mozart funcione. Solo tienes que averiguarlo.
Echando un vistazo a PHP-Scoper, y saliendo aterrado
Así que me dirigí a PHP-Scoper. Sin embargo, ni siquiera intenté probarlo, porque me asustó inmediatamente.
Para empezar, esta herramienta no soporta WordPress de forma natural. Y para continuar, recomiendan echar un vistazo a su propio Makefile, que tiene este aspecto:
# 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-versionY 600 líneas más, todas así. Parece un acertijo. Creyendo que necesitaba entender ese código solo para hacer scoping de mi plugin, salí huyendo sin ceremonia.
(Bueno, entender ese código es su recomendación para probar la aplicación scoped, pero no es obligatorio. También podemos simplemente ejecutar el comando php-scoper add-prefix, dejar que haga toda la magia, e ir a beber nuestros daiquiris.)
Volviendo a PHP-Scoper, esta vez para quedarme
Así que, hace 3 días, tomé la decisión de implementar el scoping, como fuera. Tenía que hacerlo realidad.
Volví a PHP-Scoper, para probarlo en serio. Sabía que WordPress podía scopear con él tras leer PHP Scoper: How to Avoid Namespace Issues in your Composer Dependencies (por la genial gente de Delicious Brains). Era solo cuestión de actitud, y perseverancia.
Exploré algunas de las soluciones existentes, incluyendo:
Pero ninguna me parece del todo satisfactoria: o el código parece hackish, o frágil y esperando a romperse en algún momento.
Por ejemplo, el plugin Google Web Stories hace scoping del código, y luego revierte cada uno de los conflictos:
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 );
// ...
}
]
]Entiendo por qué lo hacen, pero no me gusta. Cada vez que se referencia una nueva función de WordPress, necesitan asegurarse de añadirla también a esta lista. Es demasiado manual, demasiado frágil.
Así que este era mi reto: ¿No hay una forma más sencilla de hacer scoping de un plugin, y basándose en código que podamos presentar a nuestros amigos y colegas sin sonrojarnos?
PHP-Scoper, la forma fácil 😎
¡Resultó ser más fácil de lo que pensaba! En solo unas horas, lo tenía todo funcionando.

Ahora, cuando digo "fácil" y "horas", quiero decir: Todo funcionó inmediatamente, pero solo después de pasar 2 meses creando la estructura adecuada para el código base (lo explicaré mejor más adelante).
Pero lo importante es: Si tienes la configuración adecuada para el proyecto, hacerle scoping se puede lograr en un momento.
El problema con hacer scoping del código WordPress es, bueno, el código WordPress. El problema se explica aquí, pero se reduce a que todas las funciones y clases de WordPress también pasan a estar bajo namespace. Así que si referenciamos WP_Query o llamamos a get_posts en nuestro código, estos se transformarán en MyPrefixedNamespace\WP_Query y MyPrefixedNamespace\get_posts, produciendo un fallo épico en tiempo de ejecución. Y eso no puede evitarse en PHP-Scoper sin hacks.
Entonces, ¿cuál es la solución a esto? Facilísimo: no referencies WP_Query, ni llames a get_posts, ni uses ningún código de WordPress en el código base que vaya a ser scoped.

No, no estoy loco, y seguro que tú tampoco. Y sí, sé que estamos construyendo un plugin de WordPress... Déjame explicarte.
¿Cómo podemos no incluir código WordPress? Dividiendo el código base en 2 conjuntos de paquetes:
- Aquellos que contienen código WordPress, sin referenciar código de ninguna librería externa
- Aquellos que contienen lógica de negocio, sin contener ningún código WordPress, e incluyendo todas las dependencias requeridas y referencias a su código
De esta forma, en lugar de tener un único código base, tenemos múltiples códigos base (o paquetes), donde algunos serán scoped y otros no, y todos juntos forman el plugin, unidos vía Composer.
Entonces, no aplicamos scoping al paquete que contiene código WordPress, evitando el conflicto. Esto funciona porque no referencia ningún código que pertenezca a ninguna dependencia externa. Todas las referencias son internas, como MyNamespace\MyPlugin\MyClass. Pero estas no necesitan ser scoped, porque podemos asumir con seguridad que solo habrá 1 versión del plugin instalada en el sitio WordPress, y podemos hacer whitelist a nuestro namespace MyNamespace\*.
Además, si nuestro plugin puede ser extendido, entonces hacer whitelist a nuestro propio namespace es obligatorio. Por ejemplo, un field resolver para Gato GraphQL se implementa extendiendo de la clase PoP\ComponentModel\FieldResolvers\AbstractFieldResolver. Si le aplicara scoping, los desarrolladores se verían forzados a referenciar PoP\ComponentModel\FieldResolvers\AbstractFieldResolver para desarrollo, y PrefixedByPoP\PoP\ComponentModel\FieldResolvers\AbstractFieldResolver para producción. Eso no funciona.
Entonces, solo aplicamos scoping a los paquetes de lógica de negocio, que contienen referencias a todas las librerías externas pero ningún código WordPress.
En resumen, estamos cambiando esta estrategia:
"Tener un único código base, hacerle scoping, y luego dolorosamente y con mucha paciencia deshacer el daño, mientras rezas para que ningún conflicto pase desapercibido y 💣 explote en producción"
Por esta otra:
"Divide el código base en 2 grupos, haz scoping solo del que contiene las referencias a las dependencias externas y ningún código WordPress, y ve a por tu bien merecido daiquiri 🍹".
Enséñame lo bueno de verdad
Es hora de abrir la salchicha y ver si tiene carne de verdad dentro 🌭.
Hace 4 días, tenía el siguiente código en mi plugin:
namespace GraphQLAPI\GraphQLAPI\ContentProcessors;
use Parsedown;
class MarkdownContentParser
{
protected function getHTMLContent(string $fileContent): string
{
return (new Parsedown())->text($markdownContent);
}
}La clase Parsedown viene de la dependencia externa erusev/parsedown, según se define en el composer.json del plugin:
{
"require": {
"erusev/parsedown": "^1.7"
}
}Por tanto, mi plugin contenía referencias a una librería externa, así que necesitaba hacerle scoping, para transformar Parsedown en PrefixedByPoP\Parsedown. Pero hacerlo también haría scoping de todo el código WordPress del plugin, causando los conflictos.
Así que extraje el código a un paquete separado, llamado graphql-api/markdown-convertor, y reemplacé la dependencia de terceros en composer.json por mi propia dependencia:
{
"require": {
"graphql-api/markdown-convertor": "^0.8"
}
}Ahora, el plugin evita referenciar la librería externa; en su lugar, referencia el servicio MarkdownConvertorInterface del nuevo paquete:
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 referencia a la dependencia de terceros se hace en el nuevo paquete:
namespace GraphQLAPI\MarkdownConvertor;
use Parsedown;
class MarkdownConvertor implements MarkdownConvertorInterface
{
public function convertMarkdownToHTML(string $markdownContent): string
{
return (new Parsedown())->text($markdownContent);
}
}Finalmente, debemos:
- Hacer scoping de la dependencia
graphql-api/markdown-convertor - Saltar el scoping del código del plugin
- Hacer whitelist al namespace
GraphQLAPI\*, para evitar que mis propias clases sean scoped
Esta es prácticamente la estrategia. De aquí en adelante, será una repetición de esta misma idea, para eliminar todas las dependencias externas del código, hasta voilà, el plugin puede hacerse scoped.
Las dependencias a extraer son solo las de la sección require en tu fichero composer.json; para require-dev puedes mantener cualquier dependencia, externa o no, ya que no necesitamos hacer scoping de las dependencias usadas para desarrollo; solo aquellas para crear y enviar el plugin, para producción, necesitan ser scoped.
Al final, el composer.json de tu plugin no debería contener ninguna dependencia externa. Para mi plugin, tiene este aspecto:
{
"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"
}
}Todos esos paquetes, con namespaces getpop, graphql-api, graphql-by-pop, y pop-schema, son míos: dependencias que contienen todo el código del plugin. Están distribuidos en distintos namespaces para gestionar mejor el código, pero no necesitas hacerlo: usar un único namespace funciona bien.
Ahora, a medida que crece el número de paquetes en tu aplicación, tendrás que alojarlos todos en un monorepo, o te volverás loco creando pull requests que involucren más de un paquete (créeme, he estado ahí). En mi caso, todos mis paquetes están alojados en el monorepo GatoGraphQL/GatoGraphQL, y los mantengo sincronizados a través del maravilloso Monorepo Builder (¡necesito escribir un artículo sobre esta herramienta, es un salvavidas!).
Los namespaces de estos paquetes son PoP, GraphQLAPI, GraphQLByPoP y PoPSchema. Como son míos, sé que aparecerán solo una vez en la aplicación, y por tanto puedo evitar hacerles scoping.
Para hacerlo, los añado a la whitelist en scoper.inc.php:
return [
'whitelist' => [
// Own namespaces
'PoPSchema\*',
'PoP\*',
'GraphQLByPoP\*',
'GraphQLAPI\*',
// Own container cache
'PoPContainer\*',
],
];La última entrada corresponde al contenedor de inyección de dependencias, que también necesita ser scoped. Por defecto, este contenedor se asigna el nombre ProjectServiceContainer, directamente en el namespace global. Pero PHP-Scoper no soporta hacer whitelist de clases específicas del namespace global. Por tanto, añadí el namespace artificial PoPContainer a la whitelist, y asigné este namespace al volcar el contenedor a disco:
$dumper = new PhpDumper($containerBuilder);
file_put_contents(
self::$cacheFile,
$dumper->dump(
// Save under own namespace to avoid conflicts
array('namespace' => 'PoPContainer')
)
);Puedes notar que, respecto a los paquetes, algunos terminan en -wp (como pop-schema/users-wp) mientras que otros no (como graphql-by-pop/graphql-server). Sí, lo adivinaste: los primeros contienen código WordPress y ninguna referencia a librerías externas, y los segundos pueden contener referencias a librerías externas, pero ningún código WordPress en absoluto.
Entonces, salto el scoping de los paquetes de 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')
]
];¿Qué pasa si algún paquete WordPress necesita referenciar una librería externa, y esto no puede extraerse a otro paquete? Por ejemplo, mi paquete getpop/routing-wp depende de brain/cortex, y esto es inevitable.
No puedo hacer scoping de todo el paquete, ya que getpop/routing-wp contiene código WordPress. En su lugar, lo que hago es identificar los ficheros donde se hacen esas referencias, y asegurarme de que no contengan código WordPress. Entonces puedo hacer scoping solo de esos ficheros.
En este caso, la referencia a Cortex/Brain se hace en 2 ficheros, incluyendo 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;
}
));
}
}
}¿Notas la rareza? Esta es una implementación de un hook, pero no se llama a ningún add_action, ya que no puedo tener código WordPress aquí. En su lugar, llama a la función addAction del servicio HooksAPIInterface, y este servicio es implementado por la clase HooksAPI en el paquete getpop/hooks-wp, donde sí podemos tener código 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);
}
}Ahora que el código está dividido limpiamente, podemos hacer scoping de esos 2 ficheros que referencian dependencias externas:
return [
'finders' => [
Finder::create()->append([
'vendor/getpop/routing-wp/src/Component.php',
'vendor/getpop/routing-wp/src/Hooks/SetupCortexHookSet.php',
])
]
];Antes mencioné que configurar el scoping llevó unas horas, pero solo después de 2 meses de trabajo. Bueno, este ejemplo demuestra a qué me refería: el trabajo real reside en dividir limpiamente el código base en los 2 conjuntos.
En mi caso, el trabajo llevó 2 meses porque el nivel de detalle era extremo: ¡el plugin se convirtió en una composición de 125 paquetes! Pero este es un caso excepcional, con el objetivo de que el servidor subyacente del plugin sea CMS-agnostic, para soportar una implementación para otros CMSs/frameworks simplemente reimplementando los paquetes -wp correspondientes.
(Escribí en detalle sobre esta estrategia, en los artículos Abstracting WordPress Code To Reuse With Other CMSs: Concepts e Implementation.)
Es ciertamente bastante trabajo, pero la limpieza mejorada del código hace que merezca la pena. Y no solo para hacer scoping del plugin, lo que vino como una sorpresa total para mí, y todavía me sigo riendo de mi inesperada felicidad. Por ejemplo, ejecuto PHPStan y PHPUnit por separado en el código WordPress y no-WordPress, evitándome muchos dolores de cabeza.
Una vez que el código base está ordenado, el mundo de repente se vuelve un lugar mucho mejor.
Pruebas
Entonces, ¿cómo probamos esta bestia?
La solución que se me ocurrió es basarme en Rector, la misma herramienta que uso para hacer downgrade del código desde PHP 7.4, para desarrollo, a 7.1, para producción.
La idea es la siguiente:
- Hacer scoping del plugin
- Analizarlo con Rector, aplicando cualquier regla (no importa cuál)
Si algo fue mal al hacer scoping, entonces Rector no podrá cargar alguna clase, y lanzará un error. Por ejemplo, si la clase Brain\Cortex se hizo scoped como PrefixedByPoP\Brain\Cortex, pero alguna referencia a ella quedó como Brain\Cortex, entonces el autoloading de esta clase fallará.
Esta es mi GitHub Action para pruebas (working-directory se está usando, porque estoy operando desde la raíz del monorepo, pero el scoping ocurre en la carpeta del 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: .
Y esta es mi configuración de 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/*',
]);
};Puedes notar que algunos ficheros de dependencias, como erusev/parsedown/Parsedown.php' necesitan añadirse a Option::AUTOLOAD_PATHS. Eso es porque hacer scoping del composer.json del paquete no es 100 % fiable, y entonces su autoloading puede fallar.
Cuando esto ocurra, Rector se quejará de que alguna clase falló al hacer autoload. A partir de ahí, identificamos el fichero correspondiente, y lo añadimos manualmente a las rutas de autoloading.
Mira los resultados
Este es el código fuente del plugin, y esta es su versión scoped (y degradada a PHP 7.1).
Encuentra las 7 diferencias 😁. (Te doy una pista: busca PrefixedByPoP.)
Y este es el fichero final graphql-api.zip, listo para instalarse en tu sitio.
Eso es todo. Espero que haya sido útil 😃💪🚀