Comment le plugin mappe le modèle de données WordPress au schéma GraphQL
Voici comment Gato GraphQL a mappé le modèle de données WordPress dans un schéma GraphQL correspondant.
Le modèle de données WordPress
WordPress possède les entités suivantes :
- posts
- pages
- custom posts
- éléments médias
- utilisateurs
- rôles d'utilisateur
- étiquettes
- 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 médias sont tous des custom post types, et les étiquettes et catégories sont toutes deux des taxonomies.
Voici le diagramme de base de données 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 base de données ?
Lors du mapping de la base de données WordPress dans un schéma GraphQL, respecte-t-on exactement le même diagramme ci-dessus ?
Non, ce n'est pas le cas. Alors que le diagramme de base de données est une implémentation réelle, GraphQL est une interface pour accéder aux données depuis le client. Ces deux éléments sont liés, mais ils peuvent être différents. GraphQL ne se soucie 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 à trop nous préoccuper du diagramme de base de données lors de la création du schéma GraphQL pour WordPress. De plus, nous pouvons produire un schéma GraphQL qui corrige une partie de la dette technique du modèle de données WordPress.
Mapper le modèle de données WordPress comme un schéma GraphQL
Effectuons le mapping. D'abord, nous mappons les entités d'origine en types, dans la mesure du possible. À partir de la liste des entités dans le modèle de données 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. (C'est uniquement à des fins de documentation ; le plugin lui-même n'utilise pas le SDL pour codifier le schéma : tout est du code PHP).
Voici les champs (parmi beaucoup d'autres) pour un Post :
type Post {
id: ID!
title: String
content: String
excerpt: String
date: 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 qu'une publication a un auteur, et qu'un utilisateur possède des publications :
type Post {
author: User!
}
type User {
posts: [Post]
}Les champs et les connexions peuvent également accepter des arguments. Par exemple, nous permettons que Post.dateStr soit formaté, et que User.posts puisse filtrer les entrées, limiter leur nombre et les trier :
type Post {
dateStr(format: String): Date!
}
type User {
posts(
filter: RootPostsFilterInput
pagination: PostPaginationInput
sort: CustomPostSortInput
): [Post!]!
}
input RootPostsFilterInput {
authorIDs: [ID!]
authorSlug: String
categoryIDs: [ID!]
dateQuery: [DateQueryInput!]
excludeAuthorIDs: [ID!]
excludeIDs: [ID!]
hasPassword: Boolean = false
ids: [ID!]
isSticky: Boolean
metaQuery: [CustomPostMetaQueryInput!]
password: String
search: String
status: [FilterCustomPostStatusEnum!]
tagIDs: [ID!]
tagSlugs: [String!]
}
input PostPaginationInput {
limit: Int
offset: Int
}
input CustomPostSortInput {
by: CustomPostOrderByEnum
order: OrderEnum
}
# ...Nous continuons ainsi pour toutes les entités du modèle de données WordPress. Une fois terminé, nous arriverons au schéma GraphQL pour WordPress, visible via le client Voyager (disponible sous "Interactive Schema" dans le menu du plugin) :

Ce schéma présente des similitudes avec le diagramme de base de données WordPress, mais aussi plusieurs différences. Analysons-les.
Les opérations sans entité sont mappées comme champs Root
Le diagramme de base de données WordPress représente la façon dont 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 traiter respectivement les requêtes et les mutations).
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 {
loginUser(
usernameOrEmail: 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 loginUser peut être considéré comme plus approprié que signOn.
Regroupement des éléments du schéma
Nous pouvons appliquer des améliorations pour simplifier le schéma et le rendre plus utile. Par exemple, un champ peut recevoir tous ses arguments via un objet input, qui peut être réutilisé dans plusieurs champs et facilite la visualisation du schéma :
type MutationRoot {
loginUser(input: LoginUserByInput!): User
}
input LoginUserByInput {
usernameOrEmail: String!,
password: String!
}De plus, la réponse d'une mutation peut être un objet "payload", qui en plus de retourner l'objet affecté peut également inclure le statut de l'opération et des messages d'erreur :
type MutationRoot {
loginUser(input: LoginUserByInput!): RootLoginUserMutationPayload!
}
type RootLoginUserMutationPayload {
errors: [RootLoginUserMutationErrorPayloadUnion!]
status: OperationStatusEnum!
user: User
userID: ID
}
union RootLoginUserMutationErrorPayloadUnion = GenericErrorPayload
| InvalidUserEmailErrorPayload
| InvalidUsernameErrorPayload
| PasswordIsIncorrectErrorPayload
| UserIsLoggedInErrorPayloadToutes les mutations vont sous MutationRoot
Il y a des opérations qui dépendent d'une entité, comme wp_update_post(), qui s'applique sur une publication. La mutation correspondante dans le schéma GraphQL doit être ajoutée au type MutationRoot, car c'est ainsi que GraphQL fonctionne.
Cette opération est donc mappée ainsi :
type MutationRoot {
updatePost(input: RootUpdatePostFilterInput!): PostUpdateMutationPayload!
}
input RootUpdatePostFilterInput {
categoryIDs: [ID!]
content: String
featuredImageID: ID
id: ID!
status: CustomPostStatusEnum
tags: [String!]
title: String
}Ce plugin prend également en charge les mutations imbriquées, proposées comme fonctionnalité opt-in (car ce n'est pas un comportement GraphQL standard). Ainsi, les mutations peuvent également être ajoutées sous n'importe quel type, pas seulement MutationRoot. Dans ce cas, nous obtenons :
type Post {
update(input: PostUpdateFilterInput!): PostUpdateMutationPayload!
}
input PostUpdateFilterInput {
categoryIDs: [ID!]
content: String
featuredImageID: ID
status: CustomPostStatusEnum
tags: [String!]
title: String
}Veuillez noter la différence entre les inputs RootUpdatePostFilterInput et PostUpdateFilterInput (c'est-à-dire entre les mutations depuis la racine et les mutations imbriquées) : le premier a la propriété obligatoire id pour indiquer quelle publication modifier, mais le second ne l'a pas, car il n'en a pas besoin.
Gestion des custom posts
Il n'y a pas d'héritage de types en GraphQL. Donc, 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, Page et GenericCustomPost (pour représenter tous les custom post types définis par n'importe quel thème et plugin installé) 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!
}
type GenericCustomPost 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 | Page | GenericCustomPostEt 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!
}Lors de l'exécution de la requête, nous pouvons soit sélectionner les champs en fonction du type réel, comme Post, soit en fonction de l'interface CustomPost :
{
customPosts {
__typename
...on CustomPost {
id
title
slug
status
}
...on Post {
isSticky
postFormat
}
}
}Comme on peut le constater, dans le schéma GraphQL nous devons explicitement préciser quand nous avons affaire à des posts, et quand à des custom posts, car ils ne sont pas la même chose ! Appeler ces deux de façon interchangeable est une dette technique de WordPress, que le plugin tente de corriger chaque fois que c'est possible.
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 d'un custom post est appelé customPostID et non postID (même si c'est ainsi qu'il est nommé dans la fonction WordPress mappée).
Ainsi, l'attente est toujours claire :
- Le champ
User.customPostspeut retourner une liste de n'importe quel custom post, y compris 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 étiquettes (et catégories) sous un seul type
Pourquoi le type PostTag (et de même pour PostCategory) s'appelle-t-il ainsi, au lieu de 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 étiquettes ajoutées aux posts n'apparaîtront pas lors de la récupération des étiquettes pour les produits, et vice versa (sauf si un produit utilise également la taxonomie post_tag, mais il peut alors 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 de la même table de base de données. Mais cela compte pour GraphQL, qui est fortement typé.
Ainsi, c'est une bonne décision de conception de maintenir ces entités séparées, sous leurs propres types, et d'avoir les étiquettes pour les posts retournées sous le type PostTag et, si un plugin personnalisé implémente son propre CPT de produit, il doit utiliser le type ProductTag pour ses étiquettes.
Donner aux éléments médias leur propre identité
Les entités 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 médias comme une entité distincte, et non comme des custom posts.
Cela implique les décisions suivantes pour le schéma GraphQL :
- Le type
Median'implémente pas l'interfaceCustomPostet ne fera pas partie du typeCustomPostUnion - Le type
Median'a pas de nombreux champs attendus d'un custom post type, commeexcerpt,dateetstatus. À la place, il n'a que les champs attendus d'un élément média :
type Media {
id: ID!
src: String!
width: Int
height: Int
}Identification et mapping des enums
Dans certaines situations, WordPress utilise des valeurs fixes d'un ensemble donné. Par exemple, le statut d'une publication ne peut être que "publish", "draft", "pending" ou "trash".
En 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 devraient être écrits en majuscules, comme ceci :
enum CUSTOM_POST_STATUS {
PUBLISH
DRAFT
PENDING
TRASH
}Cependant, la requête ne peut alors pas être directement utilisée pour interagir avec WordPress, car exécuter get_posts( [ "post_status" => "PUBLISH" ] ) ne fonctionne pas.
Donc, comme compromis, nous conservons ces valeurs enum en minuscules :
enum CUSTOM_POST_STATUS {
publish
draft
pending
trash
}Mapping des types supplémentaires
Les blocs ne sont pas directement visibles dans le diagramme de base de données 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 pouvons donc tout de même introduire un type Block pour les mapper :
type Post {
blocks: [Block]
}
type Block {
type: String!
attributes: JSONObject
}