Concepts, idées, stratégies
Concepts, idées, stratégiesComment le plugin mappe le modèle de données WordPress au schéma GraphQL

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 diagramme de base de données WordPress

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 :

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

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

Le schéma GraphQL pour WordPress

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

Toutes 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 | GenericCustomPost

Et 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.customPosts peut retourner une liste de n'importe quel custom post, y compris des posts et des pages, et User.posts ne retourne que des posts
  • Le champ Root.setFeaturedImageOnCustomPost peut ajouter une image mise en avant à n'importe quel custom post, c'est pourquoi il ne s'appelle pas setFeaturedImageOnPost

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 Media n'implémente pas l'interface CustomPost et ne fera pas partie du type CustomPostUnion
  • Le type Media n'a pas de nombreux champs attendus d'un custom post type, comme excerpt, date et status. À 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
}