👶🏻 Rajeunir WordPress grâce à GraphQL
WordPress est un CMS hérité : ayant été inventé il y a plus de 17 ans, il est rempli de code PHP qui, si on avait une nouvelle chance, serait codé d'une façon différente.
GraphQL est une interface moderne pour accéder aux données. Remarquez bien le mot "interface" : peu lui importe comment le système de données sous-jacent est implémenté, mais uniquement comment exposer les données.
Que se passe-t-il lorsque nous mettons ces deux ensemble ? Comment devrions-nous concevoir l'interface GraphQL pour accéder aux données de WordPress ?
Il y a quelques stratégies évidentes que nous pouvons mettre en place :
-
Respecter la tradition et fournir un mapping qui conserve le modèle de données de WordPress tel quel, incluant la dette technique qu'il a accumulée au fil des années
-
Corriger la dette technique, en fournissant une interface exposant les données de façon abstraite, pas nécessairement liée à WordPress
Les deux approches ont des avantages et des inconvénients, et il n'y a ni juste ni faux. C'est simplement une question de parti pris, de prioriser un comportement par rapport à un autre.
Pour le plugin Gato GraphQL j'ai choisi la deuxième approche, en tentant de créer un schéma GraphQL qui, bien qu'il soit basé sur WordPress et fonctionne pour WordPress, n'est pas lié à WordPress (par exemple, en supprimant les noms et relations incohérents).
Le résultat est que GraphQL rajeunit WordPress : bien que nous ayons toujours WordPress comme notre CMS sous-jacent, avec son code PHP hérité, sa couche de données peut être recréée, basée sur le bon sens, et non sur la tradition. La couche de données revient d'être adolescente, pour redevenir un tout petit enfant.

Le résultat est un schéma GraphQL représentant le modèle de données de WordPress, supportant également les mutations imbriquées.
Voyons comment cela a été réalisé.
Le modèle de données de WordPress
WordPress possède les entités suivantes :
- posts
- pages
- custom posts
- éléments de médias
- utilisateurs
- rĂ´les d'utilisateur
- tags
- catégories
- commentaires
- blocs
- propriétés meta
- autres (options, plugins, thèmes, etc)
Ces entités peuvent avoir une hiérarchie. Par exemple, post, page et éléments de médias sont tous des custom post types, et tags et catégories sont toutes deux des taxonomies.
Voici le diagramme de la base de données de WordPress, montrant comment les données de toutes les entités sont stockées :

Le mapping est-il une réplique exacte du diagramme de BD ?
Lors du mapping de la base de données WordPress dans un schéma GraphQL, le même diagramme ci-dessus est-il respecté 1 pour 1 ?
Non, il ne l'est pas. Bien que le diagramme de la base de données soit une implémentation réelle, GraphQL est une interface pour accéder aux données depuis le client. Ces deux sont liés, mais peuvent être différents. GraphQL ne se préoccupe pas de la base de données : il ne pense pas en commandes SQL, ni ne sait qu'il existe des tables de base de données appelées wp_posts et wp_users.
Nous n'avons donc pas besoin de trop nous préoccuper du diagramme de la base de données lors de la création du schéma GraphQL pour WordPress. Cela signifie que nous pouvons produire un schéma GraphQL qui corrige une partie de la dette technique du modèle de données de WordPress.
Mapper le modèle de données de WordPress en schéma GraphQL
Effectuons le mapping. Premièrement, nous mappons les entités originales comme types, autant que possible. De la liste des entités dans le modèle de données de WordPress, nous produisons les types suivants pour le schéma GraphQL :
PostPageMediaUserUserRolePostTagPostCategoryComment
Ensuite, nous ajoutons tous les champs attendus à chaque type. Pour représenter le schéma, nous pouvons utiliser le SDL, ou Schema Definition Language. (Ceci est utilisé uniquement à des fins de documentation ; le plugin lui-même n'utilise pas le SDL pour codifier le schéma : c'est tout du code PHP).
Voici les champs (parmi beaucoup d'autres) pour un Post :
type Post {
id: ID!
title: String
content: String
excerpt: String
publishedAt: Date!
}Voici les champs (parmi beaucoup d'autres) pour un User :
type User {
id: ID!
name: String
email: String!
}Nous créons également les connexions correspondantes, qui sont des champs qui retournent une autre entité (au lieu d'un scalaire, comme un nombre ou une chaîne). Par exemple, nous représentons un article ayant un auteur, et un utilisateur possédant des articles :
type Post {
author: User!
}
type User {
posts: [Post]
}Les champs et les connexions peuvent également accepter des arguments. Par exemple, nous permettons que Post.date soit formaté, et User.posts pour rechercher des entrées et limiter leur nombre :
type Post {
date(format: String): Date!
}
type User {
posts(limit: Int, search: String): [Post]
}Nous continuons à faire cela pour toutes les entités du modèle de données de WordPress. Une fois terminé, nous arriverons au schéma GraphQL pour WordPress, visible en utilisant le client Voyager (disponible comme "Interactive Schema" dans le menu du plugin) :

Ce schéma présente des similitudes avec le diagramme de la base de données de WordPress, mais aussi de nombreuses différences. Analysons-les.
Les opérations sans entité sont mappées comme champs Root
Le diagramme de la base de données de WordPress représente comment les données sont stockées, donc il n'y a pas de "début". GraphQL, en revanche, est une interface pour récupérer des données, il doit donc y avoir une étape initiale à partir de laquelle exécuter la requête.
Cette étape initiale est le type Root, ou, pour être plus précis, les types QueryRoot et MutationRoot (pour gérer les requêtes et les mutations, respectivement).
Dans ces deux types, nous mappons toutes les opérations qui ne dépendent pas d'une entité, comme lors de l'exécution de get_posts(), get_users() ou wp_signon() :
type QueryRoot {
posts: [Post]!
users: [User]!
}
type MutationRoot {
logUserIn(username: String, password: String): User
}Les champs n'ont pas besoin d'avoir le même nom ou la même signature que l'opération qu'ils représentent. Par exemple, appeler le champ logUserIn peut être considéré plus approprié que signOn.
Toutes les mutations vont sous MutationRoot
Il y a des opérations qui dépendent d'une entité, comme wp_update_post(), qui est appliquée sur un article donné. La mutation correspondante dans le schéma GraphQL doit être ajoutée au type MutationRoot, car c'est ainsi que fonctionne GraphQL.
Cette opération est alors mappée comme ceci :
type MutationRoot {
updatePost(input: {
postID: ID!,
newTitle: String,
newContent: String
}): Post
}Ce plugin supporte également les mutations imbriquées, qui sont proposées comme fonctionnalité opt-in (car ce n'est pas un comportement GraphQL standard). Les mutations peuvent alors également être ajoutées sous n'importe quel type, pas seulement MutationRoot. Dans ce cas, nous obtenons :
type Post {
update(input: {
newTitle: String,
newContent: String
}): Post!
}Gérer les custom posts
Il n'y a pas d'héritage de types dans GraphQL. Par conséquent, nous ne pouvons pas avoir un type CustomPost, et déclarer que Post et Page l'étendent.
GraphQL offre deux ressources pour compenser ce manque : les interfaces et les types union.
Pour la première, nous créons une interface CustomPost pour le schéma, déclarant tous les champs attendus d'un custom post, et nous définissons les types Post et Page pour implémenter l'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!
}Pour la seconde, nous créons un type CustomPostUnion pour le schéma retournant tous les custom post types :
union CustomPostUnion = Post | PageEt nous faisons en sorte que les champs retournent ce type lorsque c'est approprié :
type QueryRoot {
customPost(id: ID): CustomPostUnion
customPosts: [CustomPostUnion]!
}
type User {
customPosts: [CustomPostUnion]
}
type Comment {
customPost: CustomPostUnion!
}Comme on peut le remarquer, dans le schéma GraphQL nous devons explicitement affirmer quand nous traitons des posts, et quand nous traitons des custom posts, car ils ne sont pas identiques ! Appeler ces deux de façon interchangeable est une dette technique de WordPress, que nous pouvons corriger.
Pour cette raison, un custom post est toujours appelé CustomPost et non Post, un champ traitant des custom posts est toujours appelé customPosts et non posts, et un argument de champ recevant l'ID pour un custom post est appelé customPostID et non postID (même si c'est ainsi qu'il est appelé dans la fonction WordPress mappée).
L'attente est alors toujours claire :
- le champ
User.customPostspeut retourner une liste de n'importe quel custom post, incluant des posts et des pages, etUser.postsne retourne que des posts - le champ
Root.setFeaturedImageOnCustomPostpeut ajouter une image mise en avant Ă n'importe quel custom post, c'est pourquoi il ne s'appelle passetFeaturedImageOnPost
Ne pas regrouper les tags (et les catégories) sous un seul type
Pourquoi le type PostTag (et de mĂŞme pour PostCategory) s'appelle-t-il ainsi, plutĂ´t que simplement Tag ?
Parce que, lors de l'exécution de cette requête (où un produit est un CPT), les résultats du champ tags pour les posts et les produits seront toujours différents, sans chevauchement :
query {
posts {
tags {
id
name
}
}
products {
tags {
id
name
}
}
}Les tags ajoutés aux posts n'apparaîtront pas lors de la récupération des tags pour les produits, et vice versa (à moins qu'un produit utilise également la taxonomie post_tag, mais alors il peut aussi être représenté avec le type PostTag). Cela ne représente pas un gros problème dans WordPress, car ces éléments peuvent être considérés comme des lignes différentes dans la même table de base de données. Mais cela a de l'importance pour GraphQL, qui est fortement typé.
Il est donc judicieux de maintenir ces entités séparées, sous leurs propres types, et de faire en sorte que les tags pour les posts soient retournés sous le type PostTag et, si un plugin personnalisé implémente son propre CPT de produit, il doit utiliser le type ProductTag pour ses tags.
Donner aux éléments de médias leur propre identité
Les entités de médias dans WordPress sont des custom post types, uniquement parce que c'était pratique d'un point de vue d'implémentation. Cependant, le schéma GraphQL peut éviter cette dette technique, et modéliser les éléments de médias comme une entité distincte, pas comme des custom posts.
Cela implique les décisions suivantes pour le schéma GraphQL :
- Lors de l'interrogation du champ
customPosts, il ne récupérera pas les éléments de médias - Le type
Median'implémente pas l'interfaceCustomPost, et ne fera pas partie du typeCustomPostUnion - Le type
Median'a pas beaucoup de champs attendus d'un custom post type, commeexcerpt,dateetstatus. À la place, il n'a que les champs attendus d'un élément de médias :
type Media {
id: ID!
src: String!
width: Int
height: Int
}Identifier et mapper les enums
Dans certaines situations, WordPress utilise des valeurs fixes d'un ensemble donné. Par exemple, le statut d'un post ne peut être que "publish", "draft", "pending" ou "trash".
Dans GraphQL, nous pouvons les traiter comme des enums (au lieu de chaînes), et créer un type d'énumération correspondant. Suivant le standard GraphQL, les enums doivent être écrits en majuscules, comme ceci :
enum CUSTOM_POST_STATUS {
PUBLISH
DRAFT
PENDING
TRASH
}Cependant, la requête ne peut alors pas être utilisée directement pour interagir avec WordPress, car exécuter get_posts( [ "post_status" => "PUBLISH" ] ) ne fonctionne pas.
Donc, par compromis, nous conservons ces valeurs enum en minuscules :
enum CUSTOM_POST_STATUS {
publish
draft
pending
trash
}Mapper des types supplémentaires
Les blocs ne sont pas directement visibles dans le diagramme de la base de données de WordPress, car ils sont stockés dans wp_posts (il n'y a pas de table wp_blocks), mais ils constituent néanmoins une entité distincte.
Nous introduisons donc le type Block pour les mapper :
type Post {
blocks: [Block]
}
type Block {
type: String!
attributes: JSONObject
}