TDD WordPress Theme

Tanto al comenzar a desarrollar un tema desde cero como cuando ya adquirió mayor complejidad tenemos que hacer una serie de chequeos de forma manual para asegurarnos que no nos olvidamos de incluir ninguna funcionalidad o no quedó código muerto.

Obviamente una buena checklist y una organización bien planificada de nuestro código simplifica mucho el tema pero siempre es menester automatizar este tipo de tareas.

WordPress provee una Test Suit pero está orientada principalmente al testeo del funcionamiento de WordPress más que a las cuestiones atinentes al desarrollo de themes.

Si bien puede ser utilizada como base para testear nuestros temas, a mi entender, agrega demasiado overhead para lo que nos aporta. Partiendo de este presupuesto decidí comenzar a desarrollar algunos tests propios con PHPUnit.

Primeramente, basado en el checklist que antes mencioné, definí una serie de chequeos que me interesa automatizar:

  • Existe los archivos functions.php, style.css y el screenshot
  • No hay archivos sin usar
  • Existe el template correspondiente a los template tags usados. Por ej.: get_header y header.php
  • Todas las uris locales son relativas
  • Todos los scripts/styles registrados son usados y existen
  • Hay sidebars y todas son usadas en algún lado
  • Todos los espacios de menu son usados en algún momento
  • Las páginas/artículos y el sitio tienen metadatos
  • El favicon está definido y la ruta existe
  • El theme no tiene que tomar funciones de plugin:
    • Dashboard widgets
    • Custom Post Types
    • Custom Taxonomies
    • Shortcodes
    • Metaboxes
    • Social integrations
    • Bloques de Gutenberg
  • La paginación respeta la configuración de Ajustes > Lectura
  • Customizer sanitization
  • Scripts/styles externos cargados de forma agnóstica
  • No se usa @import en los stylesheet
  • Soporte para title tag
  • Uso de $content_width
  • CSS básicos definidos:
    • alignleft, aligncenter, alignright,
    • wp-caption,
    • size-full, size-large, size-medium, size-thumbnail
  • Uso de WP_Filesystem en vez de las PHP File Functions: mkdir, fopen, fread, fwrite, fputs
  • Sin estilos o scripts hardcodeados
  • Scripts cargados únicamente en footer
  • i18n

A primera vista, algunos son sencillos como la verificación de la existencia de archivos, otros van a requerir análisis del código y algunos que podrían ser verificados utilizando la API que trae WordPress contra un servidor de desarrollo o staging.

Por ahora voy a ir haciendo el desarrollo y pruebas contra la copia de desarrollo del theme de un sitio que está en producción para tener un objetivo más real aunque la idea sería, a largo plazo, constituir un repositorio agnóstico que pueda ser rápidamente clonado en nuestro proyecto.

La primera aproximación quedaría con la siguiente estructura dentro de mi workflow habitual:

  • tests/bootstrap.php
  • tests/FaviconTest.php
  • tests/MissingFilesTest.php
  • tests/StylesheetTest.php
  • composer.json
  • phpunit.xml

Y el código propiamente de cada archivo sería:

Próximamente estaré publicando más novedades.

WordPress Customizer JS API

Esta entrada es la parte 1 de 1 en la serie Wordpress Customizer JS API

A partir de la versión 4.1 de WordPress implementó una API en Javascript que permite la creación y el control de todos los elementos del Customizer además del renderizado de controles a partir de plantillas de Underscore.

Estas dos novedades permiten el manejo de los paneles, las secciones y los controles únicamente desde Javascript, abriendo un mundo de posibilidades.

La organización de la API viene dada por el uso de colecciones para agrupar las instancias de los elementos que ya existen y modelos para la creación de nuevos. Las colecciones son:

  • wp.customize.control
  • wp.customize.panel
  • wp.customize.section

En tanto, los modelos son:

  • wp.customize.Control
  • wp.customize.Panel
  • wp.customize.Section

En el caso de los controles existen modelos específicos que extienden a Control y agregan características específicas para cada tipo:

  • BackgroundControl
  • BackgroundPositionControl
  • CodeEditorControl
  • CroppedImageControl
  • DateTimeControl
  • HeaderControl
  • ImageControl
  • MediaControl
  • SiteIconControl
  • UploadControl

La utilización de las colecciones y los métodos es bastante intuitiva ya que sigue esquemas conocidos para los que tienen una base de Javascript y similares a los que se utiliza mediante la API PHP de WordPress.Por ejemplo, 

Pero, antes de meternos de lleno, necesitamos que nuestro código se cargue cuando abramos el Customizer, para ello vamos a usar el hook customize_controls_enqueue_scripts:

function wp_enqueue_customize_script() {
    wp_enqueue_script( 'wp-customizer', get_template_directory_uri() . '/scripts/customizer/main.js', [], null, true );
}

add_action( 'customize_controls_enqueue_scripts', 'wp_enqueue_customize_script' );

Paneles

Para agregar paneles invocamos el método add de la colección panel que recibe como parámetro un objeto creado a partir del modelo Panel que, a su vez, recibe los mismos parámetros que $wp_customize->add_panel():

wp.customize.panel.add(
    new api.Panel( 'wp_nuevo_panel', {
        title: 'Nuevo panel',
        priority: 25
    })
);

Lo que agregaría un panel con id wp_nuevo_panel, título Nuevo panel y prioridad 25.

A partir de que lo agreguemos podremos acceder al mismo mediante la colección de la siguiente manera:

wp.customize.panel('wp_nuevo_panel').sections();

Lo que devuelve todas las secciones anidadas en este panel.

wp.customize.panel('wp_nuevo_panel').params.title;

Que nos permite obtener el título del panel en cuestión.

Secciones

Las secciones siguen el mismo formato que los paneles:

wp.customize.section.add(
    new api.Section( 'wp_nueva_seccion', {
        title: 'Nueva seccion',
        panel: 'wp_nuevo_panel',
        priority: 25,
        customizeAction: 'Personalización'
    })
);

Lo único particular que vemos es la opción customizeAction que representa el texto que aparece en la parte de arriba del título de la sección y que es necesario definirlo, por ahora, porque no tiene valor predeterminado.

Al igual que con los paneles, una vez agregada la sección, podemos acceder a la misma a través de la correspondiente colección:

wp.customize.sections('wp_nueva_seccion').priority(30);

Lo que cambiaría la prioridad asignada al panel cambiando la posición del mismo. O:

wp.customize.sections('wp_nueva_seccion').controls();

Para obtener la colección de controles que hay en la sección.

Controles

Llegamos a la parte más interesante del Customize, los controles. Por ahora vamos a ver la interacción desde la API y más adelante, en otro artículo, las potencialidades de las templates de Underscore.

En este caso nos encontraremos con más particularidades que con los otros dos apartados porque los controles, para que sean finalmente almacenados en nuestra BBDD, tienen estar relacionados con una setting. Si bien se pueden agregar settings de forma dinámica, por ahora, sigue siendo más sencillo agregarlas desde PHP y utilizarlas desde Javascript.

wp.customize.control.add(
    new api.Control( 'wp_nueva_control', {
        label: 'Texto',
        section: 'wp_nueva_seccion',
        setting: wp.customize( 'mi_setting_de_texto' )
    })
);

Como vemos, un sencillo control de texto, no nos presenta ninguna dificultad. Lo único nuevo es la forma de pasar la setting y se debe a que wp.customize es, a su vez, la colección donde se almacenan las settings.

Pero, por ejemplo, cuando queremos agregar un MediaControl necesitamos especificar además el texto de las etiquetas:

wp.customize.control.add(
    new api.MediaControl( 'anred_placemark_image_control', {
        section: 'wp_nueva_seccion',
        label: 'Logo',
        setting: api( 'wp_logo_image' ),
        button_labels: {
            change: 'Cambiar logo',
            default: 'Sin logo',
            frame_button: 'Elegir logo',
            frame_title: 'Elegir logo',
            placeholder: 'Logo no elegido',
            remove: 'Quitar logo',
            select: 'Elegir logo'
        }
    }
)

Hasta el momento no encontré ninguna referencia sobre qué parámetros tienen valor por defecto y cuales no para cada control. La fuente última para saberlo es el código de la API que se encuentra en wp-admin/js/customize-controls.js o el código PHP de los controles en la carpeta wp-includes/customize/.

WordPress y PHPUnit

PHPUnit es uno de los frameworks para test unitarios automáticos para PHP más usados y el elegido por los desarrolladores de WordPress junto con QUnit para probar la plataforma. Para facilitar las pruebas, los desarrolladores han creado ya todo un conjunto de test y clases auxiliares que nos resultarán muy útiles en nuestro workflow.

Instalación

Primero, necesitamos instalar PHPUnit en su versión 6 ya que la 7 no es soportada por WordPress. Hay distintas formas documentadas en el sitio del framework y podemos elegir la que más se adapte a nuestro entorno.

Segundo, creamos una nueva base de datos separada para los tests porque que la suite va a borrar todos los datos de las tablas de la instalación donde se ejecute.

Tercero, no es necesario pero definitivamente es la mejor forma de manejar el entorno de pruebas vamos a instalar wp-cli. Nuevamente, hay varías formas de llevar adelante la tarea y va a depender de nuestro entorno el método que elijamos.

Por mi parte, me ha dado mejores resultados la instalación recomendada: descargar el .phar, darle permisos de ejecución y ponerlo en una carpeta que esté en PATH.

Cuarto, nos situamos en la carpeta de la instalación de wordpress para utilizar las opciones del paquete scaffold de wp-cli para generar los archivos necesarios para instalar los tests según sea un plugin o un theme.

wp scaffold plugin-tests <nombre plugin>
wp scaffold theme-tests <nombre theme>

Quinto, ejecutamos el archivo generado dentro de la carpeta del plugin/theme en el paso anterior para instalar el entorno de pruebas. Vamos a necesitar las credenciales y datos de acceso a la base de datos que creamos antes.

bin/install-wp-tests.sh <nombre bbdd> <usuario> <contraseña> <host>

Con esto queda todo listo para que empecemos a desarrollar nuestros tests y ponerlos en la carpeta homónima para que PHPUnit los encuentre.

Para más información sobre los unit test en WordPress estos artículos van a dar una idea de las posibilidades:

Workflow en WordPress

La estructura de directorios que uso en mi workflow se aprovecha de una feature de WordPress que permite distribuir grupos de themes. La idea es que en vez de almacenar un theme por directorio se puedan poner varios usando subdirectorios.

El funcionamiento de esta feature es bastante sencillo, cuando WP lista las carpetas en wp-content/themes para ver que themes hay instalados, revisa si en la raíz  de las mismas está el archivo style.css; si no lo encuentra busca en los subdirectorios de primer nivel de la misma por si es un grupo de themes.

La idea entonces es que en nuestro entorno de desarrollo ese subdirectorio, que WP va a identificar como el que contiene el theme, sea el de las builds y que cualquier otro subdirectorio de primer nivel no tenga un archivo style.css.

  • build
  • dist/theme
  • src
    • images
    • languages
    • scripts
      • admin
      • customize
      • modules
    • styles
    • templates
      • admin
      • customize
      • modules
    • vendor
  • tasks

Build y Dist

Como se deduce de sus nombre, build, contiene las builds con todo el código sin comprimir y preparado para debug mientras que en dist se guarda el theme listo para producción.

Para evitar confundir a WP, es importante que en la raiz del directorio dist no se encuentre el style.css sino en un subdirectorio de este. Esto, además, nos simplifica el proceso de crear el zip / tar.gz para distribuir porque ya nos queda todo en un directorio listo para comprimir.

Src

Los “fuentes” del theme en el que estamos trabajando: php, imágenes y scss sin procesar, archivos de traducción, étc. Casi todo lo que está en este directorio va a ser procesado, optimizado, comprimido y demás operaciones que van a dar como resultado nuestro theme.

La idea original del directorio images era, justamente, guardar imágenes que se usaran en el diseño del theme pero, hoy en día, con los avances de CSS únicamente almacena el screenshot.

La carpeta templates contiene todo el código php, el ordenamiento interno tiene que cumplir con las limitaciones y recomendaciones de cualquier theme.

Me gusta mucho trabajar con el customizer así que suelo generar bastante código para este y, por lo tanto, tanto en la carpeta de scripts como de templates lo guardo en un directorio separado.

Además, a lo largo del tiempo, he creado algunos snipets de código que utilizo tan habitualmente como preload y lazyload, google fonts, étc que los mantego de forma separada y que cuando los incluyo en un proyecto los guardo en la carpeta modules.

Por una cuestión de separación también guardo en otro directorio, admin, todo el código que está pensado únicamente para ejecutarse en el dashboard de WP.

Por último, como no siempre hay paquetes con la librería que necesito, la versión que busco o no quiero usar un CDN, o por lo que sea, tengo el directorio vendor que se copia íntegramente tanto a las build como a la dist.

Tasks

Las tasks de Gulp.js: build, clean, clean-vendor, images, scripts, styles, templates, translation, vendor, watch.

Mantengo separadas las partes de código propio del código de vendor: build y clean para uno y vendor y clean-vendor para el otro. Ídem con las traducciones: translation.

Normalmente, build se encarga tanto de generar tanto la versión de pruebas como la versión para distribución. Antes las generaba con tareas específicas pero siempre se terminaban dando dos escenarios: dos tareas muy parecidas que había que mantener sincronizadas manualmente o una tarea con un montón de código duplicado y un enredo de condicionales.

La más importante de las tareas es watch que monitorea los cambios y actualiza la build. Para esto utilizo el modulo que trae Gulp por defecto en conjunto con BrowserSync para la inyección del nuevo CSS pero sin actualización automática. Pero, cuando trabajo sobre el customizer, desactivo completamente BS porque no se llevan bien y se suele quedar con pantallas en blanco u errores.

Gists

WP_Deduplicator: Clase estática que permite llevar el control de los post mostrados para evitar duplicar el contenido.

WP_Keepalive: Función que agrega o modifica la cabecera Connection para informar al cliente que es posible reutilizar la conexión abierta ahorrándonos establecer una conexión nueva para cada archivo.

WP_Clipboard: Función que usa una lista blanca para filtrar las etiquetas HTML del texto que se pega en el editor.

Crear un child theme en WordPress

La creación de un child theme nos da la posibilidad de adaptar un theme pre existente a nuestro gusto. Cambiar algunas fuentes, espaciado, colores e incluso funciones de un theme que nos gusta para no tener que escribirlo de cero.

La principal ventaja de hacerlo a través de un child theme, en vez de modificar el theme original, radica en que cuando se actualice el theme original no perderemos nuestras modificaciones obteniendo así lo mejor de dos mundos, actualizaciones y personalización.

Como desventaja, agregamos una capa más que tiene que ser ejecutada sobre el theme original lo que podría, aunque no debería, hacer más lenta la carga del sitio. Además, dependiendo de la profundidad de las modificaciones, puede ocurrir que una actualización del theme original entre en conflicto con el child theme pero, nuevamente, es raro que ocurra.

El principio de funcionamiento de los child themes es que cualquier template que tengamos en nuestro theme, será ejecutado en vez del homónimo en el theme original. Es decir, si en nuestro child theme creamos un comments.php, este será ejecutado en vez del archivo incluido en el parent theme.

La única excepción a esto son los archivos functions.php que ámbos serán ejecutados, primero el del child theme y luego el del parent.

Para poder crear un child theme es necesario que el tema “padre” o parent esté instalado y tener acceso al directorio themes de tu sitio con permisos de escritura.

Los pasos serían:

  1. Accede a la carpeta donde tienes alojado los themes.
  2. Crea una nueva carpeta donde se alojará tu child-theme. Si no se te ocurre ningun nombre, una buena opción es <nombre del theme original>-child
  3. Crea los archivos functions.php y style.css
  4. Agrega el contenido mínimo a cada archivo
/*
 Theme Name:   <nombre de nuestro theme>
 Theme URI:    <url>
 Description:  <descripción>
 Author:       <autor>
 Author URI:   <url-autor>
 Template:     <nombre de la carpeta del theme padre>
 Version:      <versión de nuestro theme>
 License:      <licencia>
 License URI:  <url de la licencia>
 Text Domain:  <text domain>
*/

El nombre del theme tiene que ser único, no puede ser el mismo del parent.

En template, va únicamente el nombre de la carpeta del theme para el que estamos creando el child.

Al momento de elegir licencia es importante tener en cuenta que sea compatible con la licencia del tema original.

Si no vamos a cambiar los strings del theme original, no hace falta especificar el text-domain.

<?php
add_action( 'wp_enqueue_scripts', 'my_theme_enqueue_styles' );
function my_theme_enqueue_styles() {
    wp_enqueue_style( '<parent-theme>', get_template_directory_uri() . '/style.css' );
 
    wp_enqueue_style( 'child-style',
        get_stylesheet_directory_uri() . '/style.css',
        array( '<parent-theme>' ),
        wp_get_theme()->get('Version')
    );
}

En los child themes get_template_directory_uri() nos devuelve el path al directorio del theme original y get_stylesheet_directory_uri() nos devuelve el path al directorio del child theme.

¿Por qué cargamos el stylesheet del parent theme si ya lo hace este automáticamente? Porque los hooks, a menos que les indiquemos específicamente lo contrario, se ejecutan en el orden en que fueron cargados. Entonces, si no agregamos el stylesheet del parent theme en nuestro functions.php, este se va a cargar después del nuevo stylesheet haciendo que los cambios no se vean.

Es importante que hagamos el enqueue del parent theme con el mismo handle que en el theme original para evitar que se cargue dos veces. Si estuviéramos extendiendo o modificando el theme Divi, habría que reemplazar parent-theme por divi-style. Este dato lo podemos encontrar en el propio functions.php del parent theme.

Además, como se muestra en el ejemplo, es importante utilizar en el nombre de las funciones que creemos un prefijo único para evitar que colisionen con otras funciones del parent theme u otros plugins.

Una vez cargados los archivos, y las modificaciones del caso, el theme aparecerá junto a los demás en la sección Apariencia. Es importante recordar que no podemos eliminar el parent theme mientras esté activo el child.

Evitar los post duplicados al utilizar múltiples loops en WordPress

Durante el desarrollo de un theme de WordPress es habitual que lleguemos a una instancia en la que el loop por defecto se queda corto y necesitamos implementar nuevas querys para poder mostrar la información de la forma deseada.

Al ser independientes las distintas querys entre sí puede ocurrir que traigan más de una vez el mismo post a colación provocando que se vean repetidos en nuestro sitio. Para poder tener mayor control sobre qué post se incluyen en los resultados del query WP_Query pone a nuestra disposición la opción post__not_in que nos permite especificar los ID de los post que queremos dejar fuera del loop.

Ahora el problema que nos surge es cómo llevar el control de los posts mostrados en las distintas partes del template. Para este fin existen distintos enfoques, por ejemplo, tener una variable global en donde vayamos almacenando los distintos posts que se ya se hayan mostrado.

En mi caso, utilizo una sencilla clase que incluyo en el functions.php y que utiliza una variable y un par de funciones estáticas para llevar el control de los post ya mostrados:

Como se puede observar alcanza con llamar a WP_Deduplicator::add() después de haber cargado los datos del post o pasandole directamente el ID a excluir y luego, al hacer nuestro próximo query pasar a la opción post__not_in el retorno de WP_Deduplicator::get().