👶🏻 Rejuvenescendo o WordPress com GraphQL
O WordPress é um CMS legado: tendo sido criado há mais de 17 anos, está repleto de código PHP que, dada uma nova oportunidade, seria escrito de forma diferente.
O GraphQL é uma interface moderna para acessar dados. Repare bem na palavra "interface": não importa como o sistema de dados subjacente está implementado, mas apenas como expor os dados.
O que acontece quando juntamos esses dois? Como devemos projetar a interface GraphQL para acessar dados do WordPress?
Há algumas estratégias óbvias que podemos adotar:
-
Respeitar a tradição e fornecer um mapeamento que mantenha o modelo de dados do WordPress como está, incluindo o débito técnico acumulado ao longo dos anos
-
Corrigir o débito técnico, fornecendo uma interface que exponha os dados de forma abstrata, não necessariamente vinculada ao WordPress
Ambas as abordagens têm benefícios e desvantagens, e não existe certo ou errado. É simplesmente uma questão de escolhas, priorizando um comportamento em detrimento de outro.
Para o plugin Gato GraphQL escolhi a segunda abordagem, tentando criar um schema GraphQL que, embora seja baseado no WordPress e funcione para o WordPress, não esteja atrelado ao WordPress (por exemplo, removendo nomes e relacionamentos inconsistentes).
O resultado é que o GraphQL rejuvenece o WordPress: embora ainda tenhamos o WordPress como nosso CMS subjacente, com seu código PHP legado, sua camada de dados pode ser criada do zero, baseada no bom senso, não na tradição. A camada de dados volta de ser uma adolescente para se tornar uma criança novamente.

O resultado é um schema GraphQL que representa o modelo de dados do WordPress, suportando também mutations aninhadas.
Vamos ver como isso foi realizado.
O modelo de dados do WordPress
O WordPress possui as seguintes entidades:
- posts
- páginas
- custom posts
- elementos de mídia
- usuários
- papéis de usuário
- tags
- categorias
- comentários
- blocos
- propriedades meta
- outras (opções, plugins, temas, etc.)
Essas entidades podem ter uma hierarquia. Por exemplo, post, página e elementos de mídia são todos custom post types, e tags e categorias são ambas taxonomias.
Este é o diagrama do banco de dados do WordPress, mostrando como os dados de todas as entidades são armazenados:

O mapeamento é uma réplica exata do diagrama do BD?
Ao mapear o banco de dados do WordPress em um schema GraphQL, o mesmo diagrama acima é respeitado 1 a 1?
Não, não é. Embora o diagrama do banco de dados seja uma implementação real, o GraphQL é uma interface para acessar os dados a partir do cliente. Os dois são relacionados, mas podem ser diferentes. O GraphQL não se preocupa com o banco de dados: não pensa em comandos SQL, nem sabe que existem tabelas de banco de dados chamadas wp_posts e wp_users.
Portanto, não precisamos nos preocupar muito com o diagrama do banco de dados ao criar o schema GraphQL para o WordPress. Isso significa que podemos produzir um schema GraphQL que corrija parte do débito técnico do modelo de dados do WordPress.
Mapeando o modelo de dados do WordPress como um schema GraphQL
Vamos fazer o mapeamento. Primeiro, mapeamos as entidades originais como tipos, tanto quanto possível. Da lista de entidades no modelo de dados do WordPress, produzimos os seguintes tipos para o schema GraphQL:
PostPageMediaUserUserRolePostTagPostCategoryComment
Em seguida, adicionamos todos os campos esperados a cada tipo. Para representar o schema, podemos usar o SDL, ou Schema Definition Language. (Isso é usado apenas para fins de documentação; o próprio plugin não usa SDL para codificar o schema: é tudo código PHP).
Estes são os campos (entre muitos outros) para um Post:
type Post {
id: ID!
title: String
content: String
excerpt: String
publishedAt: Date!
}Estes são os campos (entre muitos outros) para um User:
type User {
id: ID!
name: String
email: String!
}Também criamos as conexões correspondentes, que são campos que retornam outra entidade (em vez de um escalar, como um número ou uma string). Por exemplo, representamos um post tendo um autor, e um usuário possuindo posts:
type Post {
author: User!
}
type User {
posts: [Post]
}Campos e conexões também podem aceitar argumentos. Por exemplo, habilitamos Post.date para ser formatado, e User.posts para pesquisar entradas e limitar seu número:
type Post {
date(format: String): Date!
}
type User {
posts(limit: Int, search: String): [Post]
}Continuamos fazendo isso para todas as entidades no modelo de dados do WordPress. Quando terminarmos, chegaremos ao schema GraphQL para WordPress, visível usando o cliente Voyager (disponível como "Interactive Schema" no menu do plugin):

Este schema tem semelhanças com o diagrama do banco de dados do WordPress, mas também muitas diferenças. Vamos analisá-las.
Operações sem entidade são mapeadas como campos Root
O diagrama do banco de dados do WordPress representa como os dados são armazenados, portanto não há um "início". O GraphQL, porém, é uma interface para recuperar dados, logo deve haver um estágio inicial a partir do qual executar a query.
Esse estágio inicial é o tipo Root, ou, para ser mais preciso, os tipos QueryRoot e MutationRoot (para lidar com queries e mutations, respectivamente).
Nesses dois tipos, mapeamos todas as operações que não dependem de uma entidade, como ao executar get_posts(), get_users() ou wp_signon():
type QueryRoot {
posts: [Post]!
users: [User]!
}
type MutationRoot {
logUserIn(username: String, password: String): User
}Os campos não precisam ter o mesmo nome ou assinatura da operação que representam. Por exemplo, chamar o campo logUserIn pode ser considerado mais adequado do que signOn.
Todas as mutations ficam sob MutationRoot
Há operações que dependem de uma entidade, como wp_update_post(), que é aplicada em algum post. A mutation correspondente no schema GraphQL deve ser adicionada ao tipo MutationRoot, porque é assim que o GraphQL funciona.
Essa operação é então mapeada assim:
type MutationRoot {
updatePost(input: {
postID: ID!,
newTitle: String,
newContent: String
}): Post
}Este plugin também suporta mutations aninhadas, oferecidas como um recurso opt-in (porque esse não é o comportamento padrão do GraphQL). Assim, mutations também podem ser adicionadas sob qualquer tipo, não apenas MutationRoot. Nesse caso, obtemos:
type Post {
update(input: {
newTitle: String,
newContent: String
}): Post!
}Lidando com custom posts
Não há herança de tipos no GraphQL. Portanto, não podemos ter um tipo CustomPost e declarar que Post e Page o estendem.
O GraphQL oferece dois recursos para compensar essa falta: interfaces e tipos union.
Para o primeiro, criamos uma interface CustomPost para o schema, declarando todos os campos esperados de um custom post, e definimos os tipos Post e Page para implementar a interface:
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 o segundo, criamos um tipo CustomPostUnion para o schema que retorna todos os custom post types:
union CustomPostUnion = Post | PageE fazemos os campos retornarem esse tipo quando apropriado:
type QueryRoot {
customPost(id: ID): CustomPostUnion
customPosts: [CustomPostUnion]!
}
type User {
customPosts: [CustomPostUnion]
}
type Comment {
customPost: CustomPostUnion!
}Como pode ser observado, no schema GraphQL precisamos afirmar explicitamente quando estamos lidando com posts e quando com custom posts, pois eles não são a mesma coisa! Chamar esses dois de forma intercambiável é um débito técnico do WordPress, que podemos corrigir.
Por essa razão, um custom post é sempre chamado de CustomPost e não de Post, um campo que lida com custom posts é sempre chamado de customPosts e não de posts, e um argumento de campo que recebe o ID de um custom post é chamado de customPostID e não de postID (mesmo que seja assim que é chamado na função WordPress mapeada).
Assim, a expectativa é sempre clara:
- o campo
User.customPostspode retornar uma lista de qualquer custom post, incluindo posts e páginas, eUser.postsretorna apenas posts - o campo
Root.setFeaturedImageOnCustomPostpode adicionar uma imagem em destaque a qualquer custom post, por isso não é chamado desetFeaturedImageOnPost
Não agrupar tags (e categorias) sob um único tipo
Por que o tipo PostTag (e o mesmo vale para PostCategory) é chamado assim, em vez de simplesmente Tag?
Porque, ao executar esta query (onde um produto é um CPT), os resultados do campo tags para posts e produtos serão sempre diferentes, sem sobreposição:
query {
posts {
tags {
id
name
}
}
products {
tags {
id
name
}
}
}Tags adicionadas a posts não aparecerão ao recuperar tags para produtos, e vice-versa (a menos que um produto também use a taxonomia post_tag, mas aí também pode ser representado com o tipo PostTag). Isso não representa um grande problema no WordPress, já que esses itens podem ser considerados linhas diferentes da mesma tabela do banco de dados. Mas importa para o GraphQL, que é fortemente tipado.
Portanto, é uma boa decisão de design manter essas entidades separadas, sob seus próprios tipos, e ter as tags para posts retornadas sob o tipo PostTag e, se um plugin personalizado implementa seu próprio CPT de produto, ele deve usar o tipo ProductTag para suas tags.
Dando identidade própria aos itens de mídia
As entidades de mídia no WordPress são custom post types, apenas porque era conveniente do ponto de vista da implementação. No entanto, o schema GraphQL pode evitar esse débito técnico, e modelar os elementos de mídia como uma entidade distinta, não como custom posts.
Isso implica as seguintes decisões para o schema GraphQL:
- Ao consultar o campo
customPosts, ele não buscará elementos de mídia - O tipo
Medianão implementa a interfaceCustomPost, e não fará parte do tipoCustomPostUnion - O tipo
Medianão possui muitos campos esperados de um custom post type, comoexcerpt,dateestatus. Em vez disso, tem apenas os campos esperados de um elemento de mídia:
type Media {
id: ID!
src: String!
width: Int
height: Int
}Identificando e mapeando enums
Em algumas situações, o WordPress usa valores fixos de um determinado conjunto. Por exemplo, o status de um post pode ser apenas "publish", "draft", "pending" ou "trash".
No GraphQL, podemos tratá-los como enums (em vez de strings), e criar um tipo de enumeração correspondente. Seguindo o padrão GraphQL, os enums devem ser escritos em maiúsculas, assim:
enum CUSTOM_POST_STATUS {
PUBLISH
DRAFT
PENDING
TRASH
}No entanto, a query não pode ser usada diretamente para interagir com o WordPress, pois executar get_posts( [ "post_status" => "PUBLISH" ] ) não funciona.
Portanto, como compromisso, mantemos esses valores de enum em minúsculas:
enum CUSTOM_POST_STATUS {
publish
draft
pending
trash
}Mapeando tipos adicionais
Os blocos não são diretamente visíveis no diagrama do banco de dados do WordPress, pois são armazenados em wp_posts (não há uma tabela wp_blocks), mas ainda assim são uma entidade distinta.
Portanto, introduzimos o tipo Block para mapeá-los:
type Post {
blocks: [Block]
}
type Block {
type: String!
attributes: JSONObject
}