Saltar al contenido

Etiqueta: php

Cómo migrar un widget de WordPress a un bloque de Gutenberg

Hace varios años desarrollé dos plugins para WordPress: JS Archive List y JS Categories List. Ambos nacieron cuando los widgets de WordPress se desarrollaban prácticamente por completo en PHP: creabas una clase, extendías WP_Widget, generabas el formulario de configuración y, finalmente, renderizabas el HTML. Funcionaba bastante bien, pero WordPress ha cambiado mucho desde la llegada de Gutenberg. Así que decidí migrar ambos plugins para que pudieran utilizarse como un bloque de Gutenberg. En el caso de JS Categories List, ocurrió desde la versión 4.0 y en JS Archive List desde la versión 5.0, manteniendo además el widget anterior en PHP por compatibilidad con instalaciones existentes.

Lo interesante de esta migración no fue solo aprender a crear un bloque. También me hizo ver, de una forma diferente, cómo ha evolucionado WordPress.

Se acabaron los tiempos de plugins 100% hechos en PHP, ahora es bloque de Gutenberg en JS (React) + PHP
Se acabaron los tiempos de plugins 100% hechos en PHP, ahora es bloque de Gutenberg en JS (React) + PHP

De widgets en PHP a una interfaz en React

Un widget clásico de WordPress normalmente hacía casi todo en PHP. Por ejemplo, teníamos algo parecido a esto:

class My_Widget extends WP_Widget {

    public function widget( $args, $instance ) {
        // Obtener datos.
        // Generar HTML.
        // Mostrar el widget.
    }

    public function form( $instance ) {
        // Formulario de configuración.
    }
}

El formulario que veía el administrador, la configuración y el HTML final estaban muy ligados entre sí. Con Gutenberg la separación es diferente. Ahora puedes tener PHP encargándose de los datos y del renderizado mientras que el editor utiliza JavaScript y React para construir la interfaz de configuración.

Por esta razón, siento que WordPress cada vez se siente más como un CMS que ofrece servicios y APIs en PHP, mientras que diferentes interfaces consumen esos servicios.

Gutenberg es probablemente el mejor ejemplo, porque el editor trabaja con entidades como posts, páginas, usuarios o términos mediante @wordpress/core-dataQue internamente puede procesar los datos mediante las APIs de WordPress.

PHP no desapareció. Simplemente ya no tiene que encargarse de toda la interfaz.

¿Cómo comenzar un bloque de Gutenberg?

WordPress posee una herramienta oficial llamada @wordpress/create-block que genera la estructura inicial de un plugin con un bloque, incluyendo JavaScript, PHP, CSS y la configuración necesaria para compilar todo. Puedes crear uno ejecutando:

npx @wordpress/create-block@latest my-block --namespace=my-plugin

Luego:

cd my-block
npm start

Aunque estés migrando un plugin existente, te recomiendo ejecutar este comando al menos una vez. Es una forma sencilla de entender cuál es la estructura recomendada actualmente por WordPress.

Entre los archivos encontrarás uno particularmente importante:

block.json

Este archivo contiene la definición del bloque: nombre, atributos, scripts, estilos y demás metadatos. Por ejemplo:

{
    "apiVersion": 3,
    "name": "my-plugin/archive-list",
    "title": "Archive List",
    "category": "widgets",
    "attributes": {
        "showCount": {
            "type": "boolean",
            "default": false
        }
    }
}

WordPress actualmente recomienda registrar los bloques utilizando esta metadata y register_block_type() Desde PHP se hace asi:

add_action( 'init', function() {
    register_block_type( __DIR__ . '/build' );
} );

Con eso PHP conoce nuestro bloque y WordPress puede cargar los scripts y estilos necesarios.

La configuración del bloque de Gutenberg ahora vive en React

En un widget clásico, las opciones se construían con inputs HTML generados en PHP. En un bloque de Gutenberg puedes crear esa interfaz con los componentes que ya ofrece WordPress. Por ejemplo:

import { InspectorControls } from '@wordpress/block-editor';
import {
    PanelBody,
    ToggleControl
} from '@wordpress/components';

export default function Edit({ attributes, setAttributes }) {
    return (
        <InspectorControls>
            <PanelBody title="Options">
                <ToggleControl
                    label="Show post count"
                    checked={ attributes.showCount }
                    onChange={ ( value ) =>
                        setAttributes({ showCount: value })
                    }
                />
            </PanelBody>
        </InspectorControls>
    );
}

De esta forma, Gutenberg se encarga de gran parte de la experiencia del editor y nosotros solo debemos definir qué opciones necesita nuestro bloque.

En mis plugins esto reemplazó buena parte de los formularios que antes tenía que generar manualmente desde PHP.

Consumir los datos de WordPress desde el bloque de Gutenberg

Otra parte interesante es que no necesariamente tienes que crear tus propios endpoints para obtener información que WordPress ya conoce. Desde el editor puedes consultar entidades utilizando core-data. Por ejemplo, para obtener páginas puedes utilizar:

import { useEntityRecords } from '@wordpress/core-data';

const { records, isResolving } = useEntityRecords(
    'postType',
    'page'
);

WordPress se encarga de resolver la petición, de mantener los datos en su store y de entregarlos al componente. En un plugin como JS Categories List, el mismo concepto permite trabajar con taxonomías y categorías desde la interfaz del bloque, sin tener que imprimir previamente toda esa información en el HTML generado por PHP.

Eso también tiene otra ventaja, la nueva versión de mis plugins puede leer la información dinámicamente y cargar JavaScript y CSS solamente cuando realmente existe un bloque o widget en la página, en lugar de cargar recursos innecesariamente en todo el sitio.

No tienes que reescribir todo tu PHP

Esta fue una de las cosas que pensé inicialmente que serían más complicadas.

Migrar a un bloque de Gutenberg no significa necesariamente convertir todo el plugin a JavaScript. Puedes crear un bloque dinámico y continuar renderizando su contenido desde PHP, por ejemplo, block.json puede indicar:

{
    "render": "file:./render.php"
}

Y dentro de render.php Puedes reutilizar buena parte de la lógica que ya tenía tu antiguo widget, permitiendo tener una migración más progresiva:

<?php

$show_count = $attributes['showCount'] ?? false;

echo my_old_archive_render_function(
    $show_count
);

La interfaz del editor puede estar hecha con React, mientras que el frontend todavía utiliza funciones PHP que llevan años funcionando. Los bloques dinámicos están diseñados precisamente para casos donde el contenido debe calcularse al momento de renderizar la página, y WordPress permite hacerlo mediante un archivo render.php o un render_callback.

En mi caso, esto fue bastante útil porque no tenía sentido reescribir código estable solamente por utilizar Gutenberg.

Mantener el antiguo widget por retrocompatibilidad

También decidí no eliminar de inmediato los widgets anteriores. Tanto JS Archive List como JS Categories List mantienen una versión legacy en PHP para instalaciones que todavía la utilizan, mientras que el desarrollo nuevo está enfocado en el bloque Gutenberg.

Creo que esta es una buena estrategia para plugins que llevan muchos años publicados. Puedes modernizar el código sin afectar las instalaciones de los usuarios que configuraron el widget hace más de cinco años. Ya con el tiempo puedes dejar la implementación antigua únicamente en modo de mantenimiento.

WordPress es bastante diferente ahora

Después de hacer esta migración, creo que desarrollar para WordPress actualmente se parece bastante menos al WordPress con el que comencé hace años.

PHP continúa siendo una parte fundamental, pero ahora tenemos una capa de APIs, bases de datos, componentes y una interfaz escrita en React.

En lugar de pensar:

“Tengo que crear una página PHP para configurar esto.”

Cada vez pienso más:

“¿Qué datos o servicios debe proporcionar WordPress y cómo los va a consumir la interfaz?”

Ese cambio de mentalidad hace que Gutenberg tenga mucho más sentido. Así que si tienes un plugin antiguo basado en WP_Widget, no necesitas reescribirlo por completo de una vez. Puedes comenzar creando un bloque, mover las opciones del formulario a React y reutilizar tu código PHP para el renderizado. Luego moderniza el resto poco a poco.

Para empezar y entender cómo funciona todo, crea primero un bloque vacío con @wordpress/create-block, ejecuta npm start Y comienza a modificarlo. Es probablemente la forma más rápida de dejar de ver Gutenberg como algo extraño y comenzar a verlo simplemente como la nueva interfaz de desarrollo de WordPress.

Happy coding!

Deja un comentario

Cómo corregí una vulnerabilidad en mi plugin JS Archive List tras un reporte de WordFence

JS Archive List es un plugin que creé hace más de una década para mostrar archivos de entradas de WordPress en un formato más limpio y dinámico usando JavaScript (inicialmente era con JQuery). Pues hace unas semanas recibí un correo de un grupo de hackers y del equipo de Wordfence (correos separados) informándome de una vulnerabilidad en JS Archive List para realizar inyecciones SQL.

Para quienes no lo conocen: JS Archive List toma los años y meses archivados en la base de datos y permite generar un widget o listado que se navega sin recargar la página. Es sencillo, útil y, como muchas herramientas viejas, tenía una parte interna que había quedado congelada en el tiempo, de hecho ese código viene del fork original en el que está basado.

Cómo se descubrió la vulnerabilidad en JS Archive List

Hace unas semanas recibí un mensaje desde la plataforma de investigadores de WordFence. Ellos tienen un programa privado donde reportan vulnerabilidades a desarrolladores antes de hacerlas públicas, y me dieron un plazo de tres semanas para liberar un fix.

El problema estaba en algo básico, la forma de generar la consulta SQL. El plugin recibía un año a través de la API o URL para filtrar los archivos, pero ese valor venía directamente de la base de datos sin sanitización y se insertaba en la query. Resultado: era posible modificar la consulta enviando un año inválido, lo que abría la puerta a inyecciones SQL.

Nada glamuroso ni nada complicado. Pero sí peligroso.

Actualizando una década de código para usar $wpdb

El fix de la vulnerabilidad en JS Archive List estaba claro: había que actualizar una porción del plugin que llevaba más de diez años igual, adaptarla a las funciones seguras de $wpdb y garantizar que todas las consultas pasaran por sus métodos de preparación. Esto no solo eliminó la inyección SQL, sino que dejó la base para que futuras mejoras del plugin también sigan buenas prácticas. Y claro: ahora JS Archive List es más seguro que nunca. Debo admitir que solo en la última semana del plazo pude sentarme a corregirlo (cosas de la vida), pero una vez entré en modo mantenimiento salió bastante fluido.

Luego tuve que entrar al sistema de WordFence y anunciar que el problema estaba corregido en la última versión. Tanto el grupo de hackers como el equipo de WordFence revisaron y confirmaron que todo está bien para cerrar la alerta en el sistema mencionado.

Reflexión final

Este fue un recordatorio amable de que mantener software libre significa estar dispuesto a revisarlo, actualizarlo y cuidarlo. Si usas JS Archive List, te recomiendo actualizar a la última versión. Y si alguna vez te toca lidiar con reportes de seguridad, tómalos como una oportunidad para pulir tu código y ayudar a hacer Internet un lugar mas seguro para todos.

¿Te ha pasado algo similar? ¿Descubriste vulnerabilidades en tu propio software?
Me encantaría leer tus experiencias en los comentarios.

Deja un comentario

Class already exists en PHPUnit

Esta semana en el trabajo me topé con un error de Class already exists en PHPUnit. El cual me sorprendió porque no tenía mucho sentido:

Mockery\Exception\RuntimeException: Could not load mock class MiClase: class already exists

Entonces lo primero que pensé fue: “¡¿Cómo que ya existe si lo acabo de crear?!”. En mi trabajo estábamos creando una serie de pruebas unitarias (como siempre me ha gustado) para una nueva funcionalidad bastante grande, y usamos Mockery como herramienta para simular el comportamiento de clases y objetos. Si no lo conoces, Mockery es una librería para PHP que permite crear mocks, stubs y spies para probar clases de forma aislada, sin tener que depender de la implementación real.

¿Qué es un mock?

Tal vez te preguntes, ¿qué es eso de un mock? Bueno, imagina que tienes una clase llamadaServicioFactura que depende de ServicioCorreo para enviar correos. Pues resulta que cuando haces pruebas unitarias, no quieres que se envíen correos reales (que ningún usuario, inclusive el que está probando, reciba 30 notificaciones de prueba), tanto por costo, como por rendimiento y porque no se esta probando los correos. Entonces usas un mock de CorreoService que simula su comportamiento: no manda correos, pero finge que sí, y hasta puedes verificar si fue llamado o no. Bien útil. De esta forma, cuando se ejecuta la prueba, se ejecutará el mock e intercepta las llamadas y simula el comportamiento original.

Cómo solucionar: Class already exists en PHPUnit

Volviendo al error de Class already exists en PHPUnit, en el fondo, lo que pasaba es que tenia un conflicto con la instancia de un mock ya definido, es decir, habia definido un mock de una instancia de esa clase y aparte otro para la clase. . Si te estás preguntando qué es un mock de una instancia, son algo como esto:

$mock = mock('overload:' . MI_CLASE_MOCKEADA::class);

Cuando mezclas un instance mock con un mock normal, lo que en realidad estás haciendo es intentar redefinir la misma clase dos veces. Y eso ocasiona el error mencionado.

La solución a largo plazo es refactorizar ese código viejo para que nuestros tests no dependan de instance mocks. Pero la solución rápida y sencilla, es ejecutar cualquier prueba que use un instance mock en un proceso separado. PHPUnit hace esto muy fácil con una simple anotación:

/**
 * @runInSeparateProcess
 * @preserveGlobalState disabled
 */
public function pruebaQueIncluyeLosMockupsDeInstancia(): void
{
    // código con los mocks de instancia
}

Ya con eso deberías poder ejecutar la prueba, si necesitas mas información puedes ver la documentación oficial de PHP.Unit. Y si sigues con el problema, puedes probar con cerrar los mocks después de cada test con:

protected function tearDown(): void
{
    Mockery::close();
}

Palabras finales

¿Te ha pasado algo parecido? ¿Tienes otra solución o teoría? ¡Déjamelo en los comentarios, me encantaría leerte!

Gracias por leer hasta el final. Si te encontraste con el error Class already exists en PHPUnit y llegaste hasta aquí buscando respuestas, espero que esto haya sido útil.

Happy Testing! 🧪✨

Deja un comentario

Cambiar la versión de PHP en Laradock

Desde hace un par de años, se libera una versión de PHP cada año y por lo tengo para estar al día, es recomendable estar actualizando el código para que sea compatible con futuras versiones. Si utilizas alguna herramienta de contenedores como Docker y tienes tu stack armado allí tarde o temprano tienes que cambiar de versión. Pero si usas PHP en laradock, el proceso es muy fácil como puedes ver.

Cambio de versión de PHP en laradock

El primer paso es ir a la carpeta raíz de Laradock y buscar el archivo de variables de entorno llamado .env. Ábrelo con tu editor de textos favorito y buscar la variable de entorno denominada PHP_VERSION y escribir la versión deseada, por ejemplo:

PHP_VERSION=8.2

Reconstruir imágenes

Luego debes volver a construir los contenedores de php-fpm que es el que procesa el código PHP en laradock y el de workspace para poder ejecutar scripts de PHP como composer, phpcs, entre otros.

docker-compose build php-fpm
docker-compose build workspace

Luego para que los cambios tomen efecto, debes reiniciar los contenedores. En mi caso como uso un stack LAMP sería:

docker-compose down
docker-compose up -d nginx mariadb phpmyadmin workspace

Comprobar que la versión de PHP en laradock es correcta

Y finalmente ya todo debería estar en la versión definida. Para comprobar, puedes crear un archivo .php con la función php_info() por dentro para imprimir todos los datos de la versión. Para el caso del workspace puedes ejecutar: php –version y ver la versión instalada.

Finalmente, espero que te sirva y puedas usar PHP en laradock con la versión que desees. Si necesitas volver a la versión anterior, simplemente edita de nuevo el archivo .env y comienza de nuevo.

Deja un comentario