Blog

👶🏻 Rejuveneciendo WordPress a través de GraphQL

Leonardo Losoviz
Por Leonardo Losoviz ·

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:

  1. 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

  2. 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.

GraphQL + WordPress molan

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:

El diagrama de la base de datos de WordPress

¿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:

  • Post
  • Page
  • Media
  • User
  • UserRole
  • PostTag
  • PostCategory
  • Comment

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):

El esquema GraphQL para WordPress

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 | Page

Y 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.customPosts puede devolver una lista de cualquier custom post, incluyendo posts y páginas, y User.posts solo devuelve posts
  • el campo Root.setFeaturedImageOnCustomPost puede añadir una imagen destacada a cualquier custom post, por eso no se llama setFeaturedImageOnPost

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 Media no implementa la interfaz CustomPost, y no será parte del tipo CustomPostUnion
  • El tipo Media no tiene muchos campos esperados de un custom post type, como excerpt, date y status. 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
}

Suscríbete a nuestra newsletter

Mantente al tanto de todas las novedades de Gato GraphQL.