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:
- Antes de la llegada de WordPress 5.6 con soporte para PHP 8.0, un informe de Yoast describía cómo la ejecución de PHPStan en el núcleo de WordPress producía miles de problemas.
- Debido a que todavía soporta PHP 5.6, las suites de prueba de WordPress actualmente solo soportan PHPUnit hasta la versión 7.5, que ha llegado al final de su vida útil.
- El alcance de los plugins de WordPress a través de PHP-Scoper es muy difícil.
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:
- Codifica contra interfaces, no contra implementaciones.
- Crea paquetes y distribuirlos a través de Composer.
- 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:
- Dependency Injection de Symfony
- PHP-DI
- Aura.Di
- Contenedor (inyección de dependencia)
- Inyección de dependencia en Yii
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 claseUserAuthorizationSchemeRegistry
- El contrato
UserAuthorizationInterface
se cumple a través de la claseUserAuthorization
- 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 unint
, 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 mensajesgetPages
devuelve solo las páginasgetCustomPosts
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.
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.
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.
Deja una respuesta