← Volver al blog

Dos plugins, una librería, un sitio roto: aislar dependencias en compilación

Publicado el
4 min de lectura
--- vistas

Cada plugin de WordPress que usa Composer trae su propio directorio vendor/. En un sitio con veinte plugins, eso son veinte autocargadores registrados en un único proceso de PHP.

Para librerías distintas es solo un desperdicio. Para una librería de la que dependen dos plugins, es un error, y sus síntomas aparecen en el código de otro.

La mecánica

Los nombres de clase en PHP son globales. Cuando el autocargador del plugin A se registra primero y carga Vendor\Library\Client desde su propia copia, el autocargador del plugin B ya no se ejecuta para esa clase: la clase existe, así que B recibe en silencio la versión de A.

Si A trae la v2 y B espera la v3, B se rompe. El fallo es un error fatal sobre un método inexistente, en el plugin B, en una línea que llevaba dos años siendo correcta. Ninguno de los dos autores puede reproducirlo, porque ninguno tiene instalado el otro plugin.

Las librerías de captcha caen en esto constantemente. También los clientes HTTP, las implementaciones PSR y todo lo de las familias Symfony e Illuminate, precisamente los paquetes que un plugin arrastra sin pensarlo.

Los arreglos que no funcionan

«Basta con exigir la misma versión». No puedes. No controlas el otro plugin, su calendario de versiones, ni si sigue mantenido.

«Comprueba antes si la clase existe». class_exists() te dice que una clase está cargada, no que sea la versión que necesitas. Proteger tu propia definición solo te convierte en el plugin que usa en silencio el código equivocado.

«Carguemos nuestro vendor primero». Ahora ganas tú la carrera y se rompe el otro plugin. Eso no es un arreglo, es elegir quién sufre.

Renombrar la dependencia en compilación

La única respuesta duradera es dejar de compartir el nombre. PHP-Scoper reescribe durante la compilación los espacios de nombres de todo lo que hay en vendor/, de modo que tu copia pasa a ser TuPrefijo\Vendor\Library\Client. Dos plugins pueden entonces traer dos versiones de la misma librería sin que ninguno vea la del otro.

La configuración trata sobre todo de lo que no hay que renombrar:

$wp_classes   = json_decode(file_get_contents(__DIR__ .
    '/vendor/sniccowp/php-scoper-wordpress-excludes/generated/exclude-wordpress-classes.json'), true);
$wp_functions = json_decode(file_get_contents(__DIR__ .
    '/vendor/sniccowp/php-scoper-wordpress-excludes/generated/exclude-wordpress-functions.json'), true);
$wp_constants = json_decode(file_get_contents(__DIR__ .
    '/vendor/sniccowp/php-scoper-wordpress-excludes/generated/exclude-wordpress-constants.json'), true);

Los símbolos propios de WordPress deben quedar intactos: poner prefijo a add_action produce un plugin que carga y no hace nada. El paquete sniccowp/php-scoper-wordpress-excludes mantiene esas listas por ti.

Las interfaces y contratos normalmente también deben excluirse:

$excluded = [
    '/^Psr\\\\/',
    '/^Symfony\\\\Contracts\\\\/',
    '/^Illuminate\\\\Contracts\\\\/',
];

El razonamiento: una interfaz PSR es un vocabulario compartido. Si pones prefijo a Psr\Log\LoggerInterface, tu logger deja de satisfacer los type hints de los demás, y la interoperabilidad, el propósito entero del estándar, desaparece. Prefija implementaciones; deja los contratos en paz.

El coste, con honestidad

Aislar no es gratis.

La compilación gana un paso. El desarrollo va sin aislar y producción va aislada, y no son idénticas byte a byte. Un fallo que solo se reproduce en la compilación aislada es una tarde genuinamente desagradable.

Los nombres de clase dinámicos se rompen. new $className y call_user_func('Vendor\Thing::method') son cadenas, así que el aislador no las ve y hay que parchearlas o excluirlas. Las trazas también se vuelven más feas, porque el prefijo aparece en cada marco.

Ponlo en la balanza frente a la alternativa: un fallo que no puedes reproducir, reportado por un usuario que culpará a tu plugin de haber roto otro.

La regla general

Si tu código va a ejecutarse en un proceso que no controlas, no des por hecho que un nombre es solo tuyo. Los plugins de WordPress son el caso evidente, pero el mismo razonamiento cubre las extensiones de navegador que comparten una página, las librerías cargadas en una aplicación anfitriona y cualquier cosa inyectada en un entorno donde alguien más también inyecta.

Los nombres globales son un recurso compartido. Empaquetar una dependencia significa reclamar parte de ese espacio, y lo educado coincide aquí con lo duradero: reclamar una parte que nadie más vaya a querer.

Disponible para colaboración por contrato

Estoy disponible para colaborar por contrato. Si tiene una idea de proyecto interesante, reserve una llamada por Calendly.

Agenda una llamada de 30 min