Blog

👶🏻 Rejuvenescendo o WordPress com GraphQL

Leonardo Losoviz
Por Leonardo Losoviz ·

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:

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

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

GraphQL + WordPress rock

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 diagrama do banco de dados do WordPress

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:

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

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

O schema GraphQL para WordPress

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

E 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.customPosts pode retornar uma lista de qualquer custom post, incluindo posts e páginas, e User.posts retorna apenas posts
  • o campo Root.setFeaturedImageOnCustomPost pode adicionar uma imagem em destaque a qualquer custom post, por isso não é chamado de setFeaturedImageOnPost

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 Media não implementa a interface CustomPost, e não fará parte do tipo CustomPostUnion
  • O tipo Media não possui muitos campos esperados de um custom post type, como excerpt, date e status. 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
}

Assine nossa newsletter

Fique por dentro de todas as atualizações do Gato GraphQL.