👶🏻 Rejuveneciendo WordPress a través de GraphQL
WordPress es un CMS heredado: habiendo sido inventado hace más de 17 años, está lleno de código PHP que, dada una nueva oportunidad, se programaría de una forma diferente.
GraphQL es una interfaz moderna para acceder a datos. Por favor fíjate en la palabra "interfaz": no le importa cómo se implementa el sistema subyacente de datos, sino solo cómo exponer los datos.
¿Qué pasa cuando juntamos estos dos? ¿Cómo deberíamos diseñar la interfaz GraphQL para acceder a datos desde WordPress?
Hay un par de estrategias obvias que podemos poner en marcha:
-
Respetar la tradición, y proporcionar un mapeo que mantenga el modelo de datos de WordPress tal cual, incluyendo la deuda técnica que acumuló durante los años
-
Arreglar la deuda técnica, proporcionando una interfaz que exponga los datos de una forma abstracta, no necesariamente fijada a WordPress
Ambos enfoques tienen beneficios y desventajas, y no hay correcto ni incorrecto. Es solo opinionatedness, priorizar un comportamiento sobre otro.
Para el plugin Gato GraphQL he elegido el segundo enfoque, intentando crear un esquema GraphQL que, aunque se basa en WordPress y funciona para WordPress, no está atado a WordPress (por ejemplo, eliminando nombres y relaciones inconsistentes).
El resultado es que GraphQL rejuvenece WordPress: aunque seguimos teniendo WordPress como nuestro CMS subyacente, con su código PHP heredado, su capa de datos puede crearse de nuevo, basada en el sentido común, no en la tradición. La capa de datos vuelve de ser un adolescente, a ser un niño pequeño de nuevo.

El resultado es un esquema GraphQL que representa el modelo de datos de WordPress, y que también soporta nested mutations.
Veamos cómo se llevó a cabo.
El modelo de datos de WordPress
WordPress tiene las siguientes entidades:
- posts
- páginas
- custom posts
- elementos de media
- usuarios
- roles de usuario
- tags
- categorías
- comentarios
- bloques
- propiedades meta
- otros (opciones, plugins, themes, etc)
Estas entidades pueden tener una jerarquía. Por ejemplo, post, página y elementos de media son ambos custom post types, y tags y categorías son ambas taxonomías.
Este es el diagrama de la base de datos de WordPress, mostrando cómo se almacenan los datos para todas las entidades:

¿Es el mapeo una réplica exacta del diagrama de BD?
Al mapear la base de datos de WordPress a un esquema GraphQL, ¿se respeta el mismo diagrama de arriba 1 a 1?
No, no se hace. Aunque el diagrama de la base de datos es una implementación real, GraphQL es una interfaz para acceder a los datos desde el cliente. Estos dos están relacionados, pero pueden ser diferentes. A GraphQL no le importa la base de datos: no piensa en comandos SQL, ni sabe que hay tablas de base de datos llamadas wp_posts y wp_users.
Así que no necesitamos preocuparnos demasiado por el diagrama de la base de datos al crear el esquema GraphQL para WordPress. Eso significa que podemos producir un esquema GraphQL que arregle parte de la deuda técnica del modelo de datos de WordPress.
Mapeando el modelo de datos de WordPress como un esquema GraphQL
Hagamos el mapeo. Primero, mapeamos las entidades originales como tipos, tanto como sea posible. De la lista de entidades en el modelo de datos de WordPress, producimos los siguientes tipos para el esquema GraphQL:
PostPageMediaUserUserRolePostTagPostCategoryComment
Luego, añadimos todos los campos esperados a cada tipo. Para representar el esquema, podemos usar el SDL, o Schema Definition Language. (Esto se usa solo con propósitos de documentación; el plugin en sí no usa SDL para codificar el esquema: es todo código PHP).
Estos son los campos (entre muchos otros) para un Post:
type Post {
id: ID!
title: String
content: String
excerpt: String
publishedAt: Date!
}Estos son los campos (entre muchos otros) para un User:
type User {
id: ID!
name: String
email: String!
}También creamos las conexiones correspondientes, que son campos que devuelven otra entidad (en lugar de un escalar, como un número o una cadena). Por ejemplo, representamos que una entrada tiene un autor, y un usuario es dueño de entradas:
type Post {
author: User!
}
type User {
posts: [Post]
}Los campos y conexiones también pueden aceptar argumentos. Por ejemplo, habilitamos que Post.date se pueda formatear, y User.posts para buscar entradas y limitar su número:
type Post {
date(format: String): Date!
}
type User {
posts(limit: Int, search: String): [Post]
}Seguimos haciendo esto para todas las entidades del modelo de datos de WordPress. Una vez terminado, llegaremos al esquema GraphQL para WordPress, como puede verse usando el cliente Voyager (disponible como "Interactive Schema" en el menú del plugin):

Este esquema tiene similitudes con el diagrama de la base de datos de WordPress, pero también muchas diferencias. Analicémoslas.
Las operaciones sin entidad se mapean como campos Root
El diagrama de la base de datos de WordPress representa cómo se almacenan los datos, así que no hay "comienzo". GraphQL, sin embargo, es una interfaz para recuperar datos, por lo tanto debe haber una etapa inicial desde la que ejecutar la consulta.
Esta etapa inicial es el tipo Root, o, para ser más preciso, los tipos QueryRoot y MutationRoot (para tratar con queries y mutations, respectivamente).
En estos dos tipos, mapeamos todas las operaciones que no dependen de una entidad, como al ejecutar get_posts(), get_users() o wp_signon():
type QueryRoot {
posts: [Post]!
users: [User]!
}
type MutationRoot {
logUserIn(username: String, password: String): User
}Los campos no necesitan tener el mismo nombre o firma que la operación que representan. Por ejemplo, llamar al campo logUserIn puede considerarse más adecuado que signOn.
Todas las mutations van bajo MutationRoot
Hay operaciones que sí dependen de una entidad, como wp_update_post(), que se aplica sobre alguna entrada. La mutation correspondiente en el esquema GraphQL debe añadirse al tipo MutationRoot, porque así es como funciona GraphQL.
Entonces, esta operación se mapea así:
type MutationRoot {
updatePost(input: {
postID: ID!,
newTitle: String,
newContent: String
}): Post
}Este plugin también soporta nested mutations, que se ofrecen como característica opt-in (porque este no es un comportamiento estándar de GraphQL). Entonces, las mutations también pueden añadirse bajo cualquier tipo, no solo MutationRoot. En este caso, obtenemos:
type Post {
update(input: {
newTitle: String,
newContent: String
}): Post!
}Lidiar con custom posts
No hay herencia de tipos en GraphQL. Por lo tanto, no podemos tener un tipo CustomPost, y declarar que Post y Page lo extienden.
GraphQL ofrece dos recursos para compensar esta falta: interfaces y union types.
Para el primero, creamos una interfaz CustomPost para el esquema, declarando todos los campos esperados de un custom post, y definimos los tipos Post y Page para implementar la interfaz:
interface CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}
type Post implements CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}
type Page implements CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}Para el segundo, creamos un tipo CustomPostUnion para el esquema que devuelve todos los custom post types:
union CustomPostUnion = Post | PageY hacemos que los campos devuelvan este tipo cuando sea apropiado:
type QueryRoot {
customPost(id: ID): CustomPostUnion
customPosts: [CustomPostUnion]!
}
type User {
customPosts: [CustomPostUnion]
}
type Comment {
customPost: CustomPostUnion!
}Como puede observarse, en el esquema GraphQL necesitamos aseverar explícitamente cuándo estamos tratando con posts, y cuándo con custom posts, ¡ya que no son lo mismo! Llamar a estos dos indistintamente es deuda técnica de WordPress, que podemos arreglar.
Por esta razón, un custom post siempre se llama CustomPost y no Post, un campo que trata con custom posts siempre se llama customPosts y no posts, y un argumento de campo que recibe el ID para un custom post se llama customPostID y no postID (aunque así se llama en la función WordPress mapeada).
Entonces, la expectativa es siempre clara:
- el campo
User.customPostspuede devolver una lista de cualquier custom post, incluyendo posts y páginas, yUser.postssolo devuelve posts - el campo
Root.setFeaturedImageOnCustomPostpuede añadir una imagen destacada a cualquier custom post, por eso no se llamasetFeaturedImageOnPost
No agrupar tags (y categorías) bajo un único tipo
¿Por qué el tipo PostTag (y lo mismo para PostCategory) se llama así, en lugar de simplemente Tag?
Porque, al ejecutar esta consulta (donde un producto es un CPT), los resultados del campo tags para posts y productos serán siempre diferentes, sin superposición:
query {
posts {
tags {
id
name
}
}
products {
tags {
id
name
}
}
}Los tags añadidos a entradas no aparecerán al recuperar tags para productos, y viceversa (a menos que un producto también use la taxonomía post_tag, pero entonces también puede representarse con el tipo PostTag). Esto no representa un gran problema en WordPress, ya que estos elementos pueden considerarse filas diferentes de la misma tabla de base de datos. Pero sí importa para GraphQL, que es fuertemente tipado.
Entonces, es una buena decisión de diseño mantener estas entidades separadas, bajo sus propios tipos, y hacer que los tags para posts se devuelvan bajo el tipo PostTag y, si un plugin personalizado implementa su propio CPT de producto, debe usar el tipo ProductTag para sus tags.
Dar a los elementos media su propia identidad
Las entidades media en WordPress son custom post types, solo porque era conveniente desde un punto de vista de implementación. Sin embargo, el esquema GraphQL puede evitar esta deuda técnica, y modelar los elementos media como una entidad distinta, no como custom posts.
Esto implica las siguientes decisiones para el esquema GraphQL:
- Al consultar el campo
customPosts, no obtendrá elementos media - El tipo
Mediano implementa la interfazCustomPost, y no será parte del tipoCustomPostUnion - El tipo
Mediano tiene muchos campos esperados de un custom post type, comoexcerpt,dateystatus. En su lugar, solo tiene aquellos campos esperados de un elemento media:
type Media {
id: ID!
src: String!
width: Int
height: Int
}Identificar y mapear enums
En algunas situaciones, WordPress usa valores fijos de un conjunto dado. Por ejemplo, el estado de una entrada solo puede ser "publish", "draft", "pending" o "trash".
En GraphQL, podemos tratar estos como enums (en lugar de strings), y crear un tipo de enumeración correspondiente. Siguiendo el estándar de GraphQL, los enums deben escribirse en mayúsculas, así:
enum CUSTOM_POST_STATUS {
PUBLISH
DRAFT
PENDING
TRASH
}Sin embargo, entonces la consulta no puede usarse directamente para interactuar con WordPress, ya que ejecutar get_posts( [ "post_status" => "PUBLISH" ] ) no funciona.
Así que, como compromiso, mantenemos estos valores enum en minúsculas:
enum CUSTOM_POST_STATUS {
publish
draft
pending
trash
}Mapeando tipos adicionales
Los bloques no son directamente visibles en el diagrama de la base de datos de WordPress, ya que se almacenan en wp_posts (no hay tabla wp_blocks), pero no obstante son una entidad distinta.
Por tanto, introducimos el tipo Block para mapearlos:
type Post {
blocks: [Block]
}
type Block {
type: String!
attributes: JSONObject
}