WordPress es un CMS antiguo, pero también el más utilizado. Gracias a su historia de soporte de versiones de PHP obsoletas y código heredado, aún le falta implementar prácticas de codificación modernas: la abstracción en WordPress es un ejemplo.

Por ejemplo, sería mucho mejor dividir el código base de WordPress en paquetes gestionados por Composer. O tal vez, para autocargar las clases de WordPress desde las rutas de los archivos.

Este artículo te enseñará cómo abstraer el código de WordPress manualmente y utilizar las capacidades abstracción de los plugins de WordPress.

Problemas con la integración de WordPress y las herramientas PHP

Debido a su antigua arquitectura, ocasionalmente nos encontramos con problemas al integrar WordPress con herramientas para bases de código PHP, como el analizador estático PHPStan, la librería de pruebas unitarias PHPUnit y la librería de búsqueda de espacios de nombres PHP-Scoper. Por ejemplo, considera los siguientes casos:

El código de WordPress dentro de nuestros proyectos será solo una fracción del total; el proyecto también contendrá código de negocio agnóstico del CMS subyacente. Sin embargo, por el mero hecho de tener algo de código WordPress, el proyecto puede no integrarse con las herramientas correctamente.

Debido a esto, podría tener sentido dividir el proyecto en paquetes, algunos de ellos conteniendo código de WordPress y otros teniendo solo código de negocio usando PHP «vainilla» y sin código de WordPress. De esta manera, estos últimos paquetes no se verán afectados por los problemas descritos anteriormente, sino que podrán integrarse perfectamente con las herramientas.

¿Qué es la abstracción del código?

La abstracción del código elimina las dependencias fijas del mismo, produciendo paquetes que interactúan entre sí mediante contratos. Estos paquetes pueden entonces añadirse a diferentes aplicaciones con diferentes pilas, maximizando su usabilidad. El resultado de la abstracción del código es una base de código limpiamente desacoplada, basada en los siguientes pilares:

  1. Codifica contra interfaces, no contra implementaciones.
  2. Crea paquetes y distribuirlos a través de Composer.
  3. Pega todas las partes a través de la inyección de dependencias.

Codificar contra interfaces, no contra implementaciones

Codificar contra interfaces es la práctica de usar contratos para que piezas de código interactúen entre sí. Un contrato es simplemente una interfaz de PHP (o de cualquier otro lenguaje) que define qué funciones están disponibles y sus firmas, es decir, qué entradas reciben y su salida.

Una interfaz declara la intención de la funcionalidad sin explicar cómo se implementará la funcionalidad. Al acceder a la funcionalidad a través de interfaces, nuestra aplicación puede confiar en piezas de código autónomas que cumplen un objetivo específico sin saber, o preocuparse, de cómo lo hacen. De este modo, la aplicación no necesita adaptarse para cambiar a otra pieza de código que logre el mismo objetivo, por ejemplo, de un proveedor diferente.

Ejemplo de contratos

El siguiente código utiliza el contrato CacheInterface de Symfony y el contrato CacheItemInterface de la Recomendación Estándar de PHP (PSR) para implementar la funcionalidad de la caché:

use Psr\Cache\CacheItemInterface;
use Symfony\Contracts\Cache\CacheInterface;

$value = $cache->get('my_cache_key', function (CacheItemInterface $item) {
    $item->expiresAfter(3600);
    return 'foobar';
});

$cache implementa CacheInterface, que define el método get para recuperar un objeto de la caché. Al acceder a esta funcionalidad a través del contrato, la aplicación puede ser ajena a dónde está la caché. Ya sea en la memoria, en el disco, en la base de datos, en la red o en cualquier otro lugar. Aun así, tiene que realizar la función. CacheItemInterface define el método expiresAfter para declarar cuánto tiempo debe mantenerse el elemento en la caché. La aplicación puede invocar este método sin importarle cuál es el objeto almacenado en la caché; solo le importa el tiempo que debe permanecer en ella.

Codificar contra interfaces en WordPress

Como estamos abstrayendo el código de WordPress, el resultado será que la aplicación no hará referencia al código de WordPress directamente, sino siempre a través de una interfaz. Por ejemplo, la función de WordPress get_posts tiene esta firma:

/**
 * @param array $args
 * @return WP_Post[]|int[] Array of post objects or post IDs.
 */
function get_posts( $args = null )

En lugar de invocar este método directamente, podemos acceder a él a través del contrato Owner\MyApp\Contracts\PostsAPIInterface:

namespace Owner\MyApp\Contracts;

interface PostAPIInterface
{
  public function get_posts(array $args = null): PostInterface[]|int[];
}

Ten en cuenta que la función de WordPress get_posts puede devolver objetos de la clase WP_Post, que es específica de WordPress. Al abstraer el código, tenemos que eliminar este tipo de dependencia fija. El método get_posts en el contrato devuelve objetos del tipo PostInterface, lo que le permite hacer referencia a la clase WP_Post sin ser explícito al respecto. La clase PostInterface tendrá que proporcionar acceso a todos los métodos y atributos de WP_Post:

namespace Owner\MyApp\Contracts;

interface PostInterface
{
  public function get_ID(): int;
  public function get_post_author(): string;
  public function get_post_date(): string;
  // ...
}

La ejecución de esta estrategia puede cambiar nuestra comprensión de dónde encaja WordPress en nuestra pila. En lugar de pensar en WordPress como la propia aplicación (sobre la que instalamos temas y plugins), podemos pensar en él simplemente como una dependencia más dentro de la aplicación, reemplazable como cualquier otro componente. (Aunque no vayamos a sustituir WordPress en la práctica, es reemplazable desde un punto de vista conceptual).

Creación y distribución de paquetes

Composer es un gestor de paquetes para PHP. Permite a las aplicaciones PHP recuperar paquetes (es decir, piezas de código) de un repositorio e instalarlos como dependencias. Para desacoplar la aplicación de WordPress, debemos distribuir su código en paquetes de dos tipos diferentes: los que contienen el código de WordPress y los que contienen la lógica de negocio (es decir, sin código de WordPress).

Finalmente, añadimos todos los paquetes como dependencias en la aplicación, y los instalamos a través de Composer. Dado que las herramientas se aplicarán a los paquetes de código empresarial, éstos deben contener la mayor parte del código de la aplicación; cuanto mayor sea el porcentaje, mejor. Que gestionen alrededor del 90% del código total es un buen objetivo.

Cómo extraer el código de WordPress en paquetes

Siguiendo el ejemplo de antes, los contratos PostAPIInterface y PostInterface se añadirán al paquete que contiene el código de negocio, y otro paquete incluirá la implementación de estos contratos en WordPress. Para satisfacer PostInterface, creamos una clase PostWrapper que recuperará todos los atributos de un objeto WP_Post:

namespace Owner\MyAppForWP\ContractImplementations;

use Owner\MyApp\Contracts\PostInterface;
use WP_Post;

class PostWrapper implements PostInterface
{
  private WP_Post $post;
  
  public function __construct(WP_Post $post)
  {
    $this->post = $post;
  }

  public function get_ID(): int
  {
    return $this->post->ID;
  }

  public function get_post_author(): string
  {
    return $this->post->post_author;
  }

  public function get_post_date(): string
  {
    return $this->post->post_date;
  }

  // ...
}

Al implementar PostAPI, como el método get_posts devuelve PostInterface[], debemos convertir los objetos de WP_Post a PostWrapper:

namespace Owner\MyAppForWP\ContractImplementations;

use Owner\MyApp\Contracts\PostAPIInterface;
use WP_Post;

class PostAPI implements PostAPIInterface
{
  public function get_posts(array $args = null): PostInterface[]|int[]
  {
    // This var will contain WP_Post[] or int[]
    $wpPosts = \get_posts($args);

    // Convert WP_Post[] to PostWrapper[]
    return array_map(
      function (WP_Post|int $post) {
        if ($post instanceof WP_Post) {
          return new PostWrapper($post);
        }
        return $post
      },
      $wpPosts
    );
  }
}

Uso de la inyección de dependencia

La inyección de dependencias es un patrón de diseño que permite unir todas las partes de la aplicación de forma poco acoplada. Con la inyección de dependencia, la aplicación accede a los servicios a través de sus contratos, y las implementaciones de los contratos se «inyectan» en la aplicación a través de la configuración.

Simplemente cambiando la configuración, podemos cambiar fácilmente de un proveedor de contratos a otro. Hay varias bibliotecas de inyección de dependencia que podemos elegir. Aconsejamos seleccionar una que se adhiera a las Recomendaciones Estándar de PHP (a menudo denominadas «PSR»), de modo que podamos sustituir fácilmente la biblioteca por otra si surge la necesidad. Con respecto a la inyección de dependencias, la biblioteca debe satisfacer la PSR-11, que proporciona la especificación para una «interfaz de contenedor». Entre otras, las siguientes bibliotecas cumplen con PSR-11:

Acceso a los servicios a través del contenedor de servicios

La biblioteca de inyección de dependencias pondrá a disposición un «contenedor de servicios», que resuelve un contrato en su correspondiente clase implementadora. La aplicación debe confiar en el contenedor de servicios para acceder a toda la funcionalidad. Por ejemplo, mientras que normalmente invocaríamos las funciones de WordPress directamente:

$posts = get_posts();

… con el contenedor de servicios, primero debemos obtener el servicio que satisface PostAPIInterface y ejecutar la funcionalidad a través de él:

use Owner\MyApp\Contracts\PostAPIInterface;

// Obtain the service container, as specified by the library we use
$serviceContainer = ContainerBuilderFactory::getInstance();

// The obtained service will be of class Owner\MyAppForWP\ContractImplementations\PostAPI
$postAPI = $serviceContainer->get(PostAPIInterface::class);

// Now we can invoke the WordPress functionality
$posts = $postAPI->get_posts();

Uso de DependencyInjection de Symfony

El componente Dependency Injection de Symfony es actualmente la biblioteca de inyección de dependencia más popular. Permite configurar el contenedor de servicios mediante código PHP, YAML o XML. Por ejemplo, para definir que el contrato PostAPIInterface se satisfaga a través de la clase PostAPI se configura en YAML así

services:
  Owner\MyApp\Contracts\PostAPIInterface:
    class: \Owner\MyAppForWP\ContractImplementations\PostAPI

El DependencyInjection de Symfony también permite que las instancias de un servicio se inyecten automáticamente (o «autowired») en cualquier otro servicio que dependa de él. Además, facilita la definición de que una clase es una implementación de su propio servicio. Por ejemplo, considera la siguiente configuración YAML:

services:
  _defaults:
    public: true
    autowire: true

  GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistryInterface:
    class: '\GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistry'

  GraphQLAPI\GraphQLAPI\Security\UserAuthorizationInterface:
    class: '\GraphQLAPI\GraphQLAPI\Security\UserAuthorization'
    
  GraphQLAPI\GraphQLAPI\Security\UserAuthorizationSchemes\:
    resource: '../src/Security/UserAuthorizationSchemes/*'

Esta configuración define lo siguiente:

  • El contrato UserAuthorizationSchemeRegistryInterface se cumple a través de la clase UserAuthorizationSchemeRegistry
  • El contrato UserAuthorizationInterface se cumple a través de la clase UserAuthorization
  • Todas las clases de la carpeta UserAuthorizationSchemes/ son una implementación de sí mismas
  • Los servicios deben inyectarse automáticamente entre sí (autowire: true)

Veamos cómo funciona el autocableado. La clase UserAuthorization depende de un UserAuthorizationSchemeRegistryInterface:

class UserAuthorization implements UserAuthorizationInterface
{
  public function __construct(
      protected UserAuthorizationSchemeRegistryInterface $userAuthorizationSchemeRegistry
  ) {
  }

  // ...
}

Gracias a autowire: true, el componente DependencyInjection hará que el servicio UserAuthorization reciba automáticamente su dependencia necesaria, que es una instancia de UserAuthorizationSchemeRegistry.

Cuándo hay que abstraer

Abstraer el código podría consumir un tiempo y un esfuerzo considerables, por lo que solo deberíamos llevarlo a cabo cuando sus beneficios superen sus costes. A continuación se sugieren los casos en los que abstraer el código puede merecer la pena. Puedes hacerlo utilizando los fragmentos de código de este artículo o los plugins de WordPress abstractos sugeridos a continuación.

Acceso a las herramientas

Como se ha mencionado anteriormente, ejecutar PHP-Scoperen WordPress es difícil. Al desacoplar el código de WordPress en paquetes distintivos, se hace factible el alcance de un plugin de WordPress directamente.

Reducir el tiempo y el coste de las herramientas

Ejecutar un conjunto de pruebas PHPUnit lleva más tiempo cuando tiene que inicializar y ejecutar WordPress que cuando no lo hace. Menos tiempo también puede traducirse en menos dinero gastado en la ejecución de las pruebas: por ejemplo, GitHub Actions cobra por los ejecutores alojados en GitHub en función del tiempo de uso.

No es necesaria una refactorización intensa

Un proyecto existente puede requerir una fuerte refactorización para introducir la arquitectura necesaria (inyección de dependencias, división del código en paquetes, etc.), lo que dificulta su extracción. Abstraer el código al crear un proyecto desde cero lo hace mucho más manejable.

Producción de código para múltiples plataformas

Al extraer el 90% del código en un paquete agnóstico para el CMS, podemos producir una versión de la biblioteca que funcione para un CMS o un marco de trabajo diferente sustituyendo solo el 10% de la base de código general.

Migración a una plataforma diferente

Si tenemos que migrar un proyecto de Drupal a WordPress, de WordPress a Laravel, o cualquier otra combinación, solo hay que reescribir el 10% del código, lo que supone un ahorro importante.

Mejores prácticas

Al diseñar los contratos para abstraer nuestro código, hay varias mejoras que podemos aplicar a la base de código.

Adherirse al PSR-12

A la hora de definir la interfaz para acceder a los métodos de WordPress, debemos adherirnos a la norma PSR-12. Esta reciente especificación pretende reducir la fricción cognitiva cuando se analiza el código de diferentes autores. Adherirse a PSR-12 implica cambiar el nombre de las funciones de WordPress.

WordPress nombra las funciones utilizando snake_case, mientras que PSR-12 utiliza camelCase. Por lo tanto, la función get_posts se convertirá en getPosts:

interface PostAPIInterface
{
  public function getPosts(array $args = null): PostInterface[]|int[];
}

…y:

class PostAPI implements PostAPIInterface
{
  public function getPosts(array $args = null): PostInterface[]|int[]
  {
    // This var will contain WP_Post[] or int[]
    $wpPosts = \get_posts($args);

    // Rest of the code
    // ...
  }
}

Métodos de división

Los métodos de la interfaz no tienen por qué ser una réplica de los de WordPress. Podemos transformarlos siempre que tenga sentido. Por ejemplo, la función de WordPress get_user_by($field, $value) sabe cómo recuperar el usuario de la base de datos a través del parámetro $field, que acepta los valores "id", "ID", "slug", "email" o "login". Este diseño tiene algunos problemas:

  • No fallará en el momento de la compilación si pasamos una cadena incorrecta
  • El parámetro $value necesita aceptar todos los tipos diferentes para todas las opciones, aunque al pasar "ID" espera un int, al pasar "email" solo puede recibir una cadena

Podemos mejorar esta situación dividiendo la función en varias:

namespace Owner\MyApp\Contracts;

interface UserAPIInterface
{
  public function getUserById(int $id): ?UserInterface;
  public function getUserByEmail(string $email): ?UserInterface;
  public function getUserBySlug(string $slug): ?UserInterface;
  public function getUserByLogin(string $login): ?UserInterface;
}

El contrato se resuelve para WordPress así (asumiendo que hemos creado UserWrapper y UserInterface, como se explicó anteriormente):

namespace Owner\MyAppForWP\ContractImplementations;

use Owner\MyApp\Contracts\UserAPIInterface;

class UserAPI implements UserAPIInterface
{
  public function getUserById(int $id): ?UserInterface
  {
    return $this->getUserByProp('id', $id);
  }

  public function getUserByEmail(string $email): ?UserInterface
  {
    return $this->getUserByProp('email', $email);
  }

  public function getUserBySlug(string $slug): ?UserInterface
  {
    return $this->getUserByProp('slug', $slug);
  }

  public function getUserByLogin(string $login): ?UserInterface
  {
    return $this->getUserByProp('login', $login);
  }

  private function getUserByProp(string $prop, int|string $value): ?UserInterface
  {
    if ($user = \get_user_by($prop, $value)) {
      return new UserWrapper($user);
    }
    return null;
  }
}

Eliminar los detalles de implementación de la firma de la función

Las funciones en WordPress pueden proporcionar información sobre cómo se implementan en su propia firma. Esta información puede eliminarse al valorar la función desde una perspectiva abstracta. Por ejemplo, la obtención del apellido del usuario en WordPress se realiza llamando a get_the_author_meta, haciendo explícito que el apellido de un usuario se almacena como un valor «meta» (en la tabla wp_usermeta):

$userLastname = get_the_author_meta("user_lastname", $user_id);

No es necesario transmitir esta información al contrato. Las interfaces solo se preocupan por el que, no por el cómo. Por lo tanto, el contrato puede tener un método getUserLastname, que no proporciona ninguna información sobre cómo se implementa:

interface UserAPIInterface
{
  public function getUserLastname(UserWrapper $userWrapper): string;
  ...
}

Añadir tipos más estrictos

Algunas funciones de WordPress pueden recibir parámetros de diferentes maneras, lo que lleva a la ambigüedad. Por ejemplo, la función add_query_arg puede recibir una sola clave y un valor:

$url = add_query_arg('id', 5, $url);

… o un array de key => value:

$url = add_query_arg(['id' => 5], $url);

Nuestra interfaz puede definir una intención más comprensible dividiendo dichas funciones en varias separadas, cada una de las cuales acepta una combinación única de entradas:

public function addQueryArg(string $key, string $value, string $url);
public function addQueryArgs(array $keyValues, string $url);

Eliminar la deuda técnica

La función de WordPress get_posts no solo devuelve «posts» sino también «páginas» o cualquier entidad de tipo «custom posts», y estas entidades no son intercambiables. Tanto los posts como las páginas son posts personalizados, pero una página no es un post y no es una página. Por lo tanto, ejecutar get_posts puede devolver páginas. Este comportamiento es una discrepancia conceptual.

Para hacerlo correctamente, get_posts debería llamarse get_customposts, pero nunca se le cambió el nombre en el núcleo de WordPress. Es un problema común con la mayoría del software de larga duración y se llama «deuda técnica» – código que tiene problemas, pero nunca se arregla porque introduce cambios de ruptura.

Sin embargo, al crear nuestros contratos, tenemos la oportunidad de evitar este tipo de deuda técnica. En este caso, podemos crear una nueva interfaz ModelAPIInterface que pueda tratar con entidades de diferentes tipos, y hacemos varios métodos, cada uno para tratar con un tipo diferente:

interface ModelAPIInterface
{
  public function getPosts(array $args): array;
  public function getPages(array $args): array;
  public function getCustomPosts(array $args): array;
}

De esta manera, la discrepancia no se producirá más, y verás estos resultados:

  • getPosts devuelve solo los mensajes
  • getPages devuelve solo las páginas
  • getCustomPosts devuelve tanto las entradas como las páginas

Ventajas de abstraer el código

Las principales ventajas de abstraer el código de una aplicación son:

  • Las herramientas que se ejecutan en paquetes que sólo contienen código empresarial son más fáciles de configurar y tardarán menos tiempo (y menos dinero) en funcionar.
  • Podemos utilizar herramientas que no funcionan con WordPress, como por ejemplo el scoping de un plugin con PHP-Scoper.
  • Los paquetes que producimos pueden ser autónomos para utilizarlos en otras aplicaciones fácilmente.
  • Migrar una aplicación a otras plataformas es más fácil.
  • Podemos dejar de pensar en WordPress para pensar en la lógica de nuestro negocio.
  • Los contratos describen la intención de la aplicación, haciéndola más comprensible.
  • La aplicación se organiza a través de paquetes, creando una aplicación ligera que contenga lo mínimo y mejorándola progresivamente según sea necesario.
  • Podemos eliminar la deuda técnica.

Problemas con la abstracción del código

Las desventajas de abstraer el código de una aplicación son:

  • Al principio supone una cantidad considerable de trabajo.
  • El código se vuelve más verboso; se añaden capas adicionales de código para conseguir el mismo resultado.
  • Puedes acabar produciendo docenas de paquetes que luego hay que gestionar y mantener.
  • Es posible que necesites un monorepo para gestionar todos los paquetes juntos.
  • La inyección de dependencia podría ser excesiva para aplicaciones simples (rendimientos decrecientes).
  • Abstraer el código nunca se logrará del todo, ya que suele haber una preferencia general implícita en la arquitectura del CMS.

Opciones de los plugins abstracción de WordPress

Aunque por lo general lo más sensato es extraer tu código a un entorno local antes de trabajar en él, algunos plugins de WordPress pueden ayudarte a conseguir tus objetivos de abstracción. Estas son nuestras principales selecciones.

1. WPide

Producido por WebFactory Ltd, el popular plugin WPide amplía notablemente la funcionalidad del editor de código por defecto de WordPress. Sirve como un plugin abstracto de WordPress al permitirte ver tu código in situ para visualizar mejor lo que necesita atención.

El plugin WPide.
El plugin WPide.

WPide también dispone de una función de búsqueda y sustitución para localizar rápidamente el código obsoleto o caducado y sustituirlo por una versión refactorizada.

Además de esto, WPide proporciona un montón de características adicionales, incluyendo:

  • Resaltado de sintaxis y bloques
  • Copias de seguridad automáticas
  • Creación de archivos y carpetas
  • Completo explorador de archivos
  • Acceso a la API del sistema de archivos de WordPress

2. Ultimate DB Manager

El plugin Ultimate WP DB Manager de WPHobby te ofrece una forma rápida de descargar tus bases de datos en su totalidad para su extracción y refactorización.

El plugin Ultimate DB Manager.
El plugin Ultimate DB Manager.

Por supuesto, los plugins de este tipo no son necesarios para los usuarios de Kinsta, ya que Kinsta ofrece acceso directo a la base de datos a todos los clientes. Sin embargo, si no tienes suficiente acceso a la base de datos a través de tu proveedor de alojamiento, Ultimate DB Manager podría ser útil como un plugin abstracto de WordPress.

3. Tu propio plugin de WordPress abstracto personalizado

Al final, la mejor opción para la abstracción siempre será crear tu plugin. Puede parecer una gran empresa, pero si tienes una capacidad limitada para gestionar los archivos del núcleo de WordPress directamente, esto ofrece una solución de abstracción.

Hacerlo tiene claros beneficios:

  • Extrae tus funciones de los archivos de su tema
  • Conserva tu código a través de los cambios de tema y las actualizaciones de la base de datos

Puedes aprender a crear tu plugin abstracto de WordPress a través Manual del Desarrollador de Plugins de WordPress.

Resumen

¿Debemos abstraer el código de nuestras aplicaciones? Como con todo, no hay una «respuesta correcta» predefinida, ya que depende de cada proyecto. Aquellos proyectos que requieren una enorme cantidad de tiempo para analizar con PHPUnit o PHPStan pueden ser los más beneficiados, pero el esfuerzo necesario para llevarlo a cabo no siempre merece la pena.

Has aprendido todo lo que necesitas saber para empezar a abstraer el código de WordPress.

¿Piensas aplicar esta estrategia en tu proyecto? Si es así, ¿utilizarás un plugin abstracto de WordPress? Háznoslo saber en la sección de comentarios.

Leonardo Losoviz

Leo writes about innovative web development trends, mostly concerning PHP, WordPress and GraphQL. You can find him at leoloso.com and twitter.com/losoviz.