Concepts, idées, stratégies
Concepts, idées, stratégiesMapper le schéma GraphQL pour votre site, thème ou plugin WordPress

Mapper le schéma GraphQL pour votre site, thème ou plugin WordPress

Vous avez donc décidé de commencer à utiliser GraphQL pour votre site WordPress existant. Excellent ! Qu'il soit utilisé pour de nouvelles fonctionnalités ou pour des fonctionnalités existantes, GraphQL devra interagir avec la couche de données sous-jacente, pour laquelle vous devrez mapper le modèle de données de votre application (qu'il s'agisse de code PHP personnalisé dans votre site WordPress, votre thème ou votre plugin) dans le schéma GraphQL.

Comment le mapping doit-il être effectué ? Doit-il être réalisé en une seule fois ? Doit-il être une réplique exacte du modèle de données existant ? Qu'en est-il de la correction d'un nom inapproprié au cours du processus ? Et concernant la dette technique, faut-il la conserver ou s'en occuper ?

Explorons quelques stratégies pour mapper le modèle de données d'une application WordPress existante dans un schéma GraphQL.

Mappez le schéma à votre propre rythme

Ajouter GraphQL à une application n'est pas tout ou rien. La même application pourrait être alimentée par plusieurs API simultanément, auquel cas GraphQL coexistera avec d'autres API aussi longtemps que nécessaire. Par exemple, nous pourrions conserver les fonctionnalités existantes alimentées par REST, et incorporer GraphQL uniquement pour toutes les nouvelles fonctionnalités.

Si vous souhaitez effectuer une migration complète vers GraphQL, cela ne doit pas nécessairement se faire en une seule fois. Les fonctionnalités existantes pourraient être migrées lentement mais sûrement vers GraphQL, jusqu'au jour où GraphQL deviendra la seule API de l'application.

Ainsi, même si vous pouvez créer le schéma GraphQL complet dès le premier jour, vous n'y êtes pas obligé : à tout moment donné, seules les entités requises par les fonctionnalités doivent être présentes dans le schéma (via leurs types, champs et interfaces). Vous pouvez les mapper au fur et à mesure, de façon progressive.

Ne laissez pas l'interface porter le poids de l'implémentation

Le serveur GraphQL implémentera la logique d'accès aux données de l'application. Il le fera en appelant des fonctionnalités de WordPress, comme appeler get_posts pour récupérer des données de publications. À cette couche, il y a du code PHP pour satisfaire les resolvers.

Un schéma GraphQL, cependant, est une interface : il déclare les contrats pour accéder aux données dans l'API. Il ne se soucie pas des détails d'implémentation : il ne sait rien de WordPress, ni de la fonction get_posts, de la table de base de données wp_posts ou des requêtes SQL.

En tant que tel, nous devrions éviter de faire fuiter des informations entre les couches, autant que possible.

C'est important, car le modèle de données sera souvent terni par son implémentation. WordPress en fournit un exemple clair avec le CPT "attachment", pour représenter des fichiers médias tels que des images.

Parce qu'il s'agit d'un Custom Post Type, une image est traitée comme une publication. Nous pourrions alors être tentés de représenter les fichiers médias en utilisant le type Post, qui contient ces champs :

type Post {
  id: ID!
  title: String
  content: String
  excerpt: String
}

Mais cela pourrait ne pas être approprié pour l'application. La signification du champ "content" est claire pour une publication, mais pas pour une image. Il ne devrait probablement pas s'y trouver.

Une image a été modélisée comme un CPT dans WordPress parce que c'était pratique, ainsi elle pouvait réutiliser la logique existante et être stockée dans la table wp_posts existante.

Cependant, pratique ne signifie pas approprié, et peut finalement mener à une dette technique (c'est-à-dire un code déficient qui ne peut pas être corrigé sans produire un changement disruptif, et qui est donc conservé dans l'application plus longtemps qu'il ne le devrait).

Dans la mesure du possible, nous ne voulons pas conserver de dette technique dans notre application. Chaque fois que l'occasion se présente, nous devrions la corriger. Le mapping du modèle de données vers le schéma GraphQL offre une telle opportunité, nous permettant de corriger le problème au niveau de la couche d'interface de données.

(La dette technique persistera néanmoins au niveau de l'application, donc nous ne résolvons pas complètement le problème, mais nous l'atténuons dans la mesure de nos moyens.)

Mettons cette idée en pratique. Plutôt que d'avoir un type Post représentant les fichiers médias, il est plus judicieux d'avoir un type Media, contenant uniquement les propriétés qui ont du sens pour une entité image :

type Media {
  id: ID!
  src: String!
  width: Int
  height: Int
}

Sous le capot, au niveau de l'implémentation, le field resolver continuera d'exécuter la fonction get_posts pour résoudre les entrées de type Media, mais cela ne concerne pas le schéma GraphQL.

Découpler le schéma GraphQL du diagramme de base de données

WordPress est implémenté sur ce diagramme entité-relation de base de données :

Diagramme entité-relation de base de données dans WordPress

Nous devons baser le schéma GraphQL sur le diagramme de base de données, mais nous ne devons pas tenter de créer une réplique 1 pour 1. C'est parce que le schéma GraphQL et le diagramme de base de données sont tous deux construits avec certaines préconditions ou limitations, qui ne s'appliqueront pas à l'autre.

La section précédente en démontre un exemple, où la table wp_posts stocke les données du CPT image, mais en GraphQL il y aura deux types distincts, Post et Media.

Considérons un autre exemple : les catégories. Dans WordPress, une publication peut avoir une catégorie (ou plusieurs), et tout CPT peut également créer sa propre catégorie. Par exemple, un CPT appelé "event" aura un "event_category".

Les catégories de publications et les catégories d'événements sont toutes deux stockées dans la table wp_terms. Cela facilite la récupération par WordPress de lignes de l'un ou l'autre type de catégorie lors de l'exécution de la requête SQL.

Nous pourrions donc être tentés de mapper les catégories via le type Category, référencé à la fois par les publications et les événements :

type Category {
  id: ID!
  name: String!
}
 
type Post {
  categories: [Category]!
}
 
type Event {
  categories: [Category]!
}

Cependant, une publication contiendra toujours des catégories de publications, et un événement contiendra toujours des catégories d'événements. Les données de ces deux types de catégories peuvent être stockées dans la même table de base de données, mais elles ne seront pas mélangées au niveau de l'application. Catégorie de publication et catégorie d'événement sont deux entités distinctes.

GraphQL possède un système de types statique. Pour tirer le meilleur parti de GraphQL, les différentes entités au niveau de l'application doivent être modélisées en utilisant différents types dans le schéma GraphQL.

Dans ce cas, lors du mapping des catégories dans le schéma GraphQL, nous devrions créer un type différent pour chacune d'elles : PostCategory et EventCategory. Alors, le type Post ne référencera que PostCategory, et le type Event ne référencera que EventCategory :

type PostCategory {
  id: ID!
  name: String!
}
 
type Post {
  categories: [PostCategory]!
}
 
type EventCategory {
  id: ID!
  name: String!
}
 
type Event {
  categories: [EventCategory]!
}

Si nous voulons toujours avoir une entité dans le schéma qui englobe toutes les catégories, cela peut être réalisé via une interface Category :

interface Category {
  name: String!
}
 
type PostCategory implements Category {
  id: ID!
  name: String!
}
 
type EventCategory implements Category {
  id: ID!
  name: String!
}

De cette façon, les utilisateurs accédant à l'API auront une compréhension claire des données qui seront récupérées, indépendamment de la façon dont elles sont mappées dans le diagramme de base de données et dont elles sont stockées en base de données.

Une fois que nous avons le schéma GraphQL final, nous pouvons constater que sa forme ressemblera quelque peu au diagramme de base de données de WordPress, tout en en étant clairement différente :

Schéma GraphQL

Adapter la nomenclature des champs en suivant le typage statique

Les champs devraient, dans la mesure du possible, respecter la même nomenclature qu'ils ont dans l'application.

Par exemple, nous pouvons créer une publication avec la fonction wp_insert_post, et la publication a des propriétés "title" et "content". Ces noms conviennent également pour le schéma GraphQL (même s'ils peuvent nécessiter de légères modifications), donc nous devrions les conserver :

type MutationRoot {
  insertPost(title: String, content: String): Post
}
 
type Post {
  id: ID!
  title: String
  content: String
}

Mais ce n'est pas nécessairement le cas tout le temps. Comme nous l'avons vu précédemment, les publications personnalisées doivent être découplées dans leurs propres entités. Ainsi, tandis que la fonction get_posts récupère une liste de n'importe quel CPT, un champ équivalent posts dans le type racine du schéma ne récupérera que des entités de type Post, mais pas Page (qui est également un CPT) :

type QueryRoot {
  posts: [Post]!
}

Alors, comment obtenir la liste de toutes les publications et pages ? Via un autre champ, customPosts, qui récupère les entités de n'importe quel CPT mappé sous le type union CustomPostUnion :

union CustomPostUnion = Post | Page
 
type QueryRoot {
  customPosts: [CustomPostUnion]!
}

La leçon importante est celle-ci : la nomenclature que nous choisissons pour le schéma GraphQL doit être adaptée au type de l'entité récupérée. Et en raison des types forts de GraphQL, ce type peut être différent au niveau de l'application et au niveau de l'API.

Dans ce cas, alors que dans WordPress un "post" pourrait désigner n'importe quel "custom post type", en GraphQL un "post" est nécessairement un Post. Si un champ récupère des publications personnalisées, alors le champ dans le schéma GraphQL doit s'appeler customPosts, pas posts. De même, si un input reçoit un ID pour une publication personnalisée, il doit s'appeler customPostID, pas postID.

Mapping pour le champ customPosts

Cette leçon s'applique aux commentaires, par exemple. Un commentaire peut être ajouté à n'importe quel CPT, pas seulement aux publications. Alors, le type Comment doit le préciser clairement, en contenant le champ customPost (et non post) :

type Comment {
  id: ID!
  customPost: CustomPostUnion!
}

Convertir les valeurs de chaîne prédéfinies en enums, en majuscules si possible

Les types d'énumération sont, par convention, définis en majuscules. Par exemple, la documentation de graphql.org fournit cet exemple :

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

Chaque fois que nous devons créer un nouveau type enum, nous devrions utiliser des majuscules pour ses constantes définies. Cependant, lors de la migration du modèle de données depuis l'application, nous pouvons rencontrer certains ensembles de valeurs prédéfinies que nous pouvons mapper via un enum, mais dont les valeurs sont des chaînes en minuscules.

Pour fournir un exemple, les publications dans WordPress ont une propriété "status", contenant l'une des valeurs suivantes :

  • "publish"
  • "pending"
  • "draft"
  • "trash"

Lors du mapping de cette propriété dans le schéma, le champ Post.status pourrait retourner un String, comme ceci :

type Post {
  status: String!
}

Cependant, puisque le statut sera nécessairement l'une de ces valeurs prédéfinies, et aucune autre, nous préférons le mapper comme un enum :

enum Status {
  PUBLISH
  DRAFT
  PENDING
  TRASH
}
 
type Post {
  status: Status!
}

Maintenant, nous pouvons avoir un problème : l'enum PUBLISH sera converti en la valeur de chaîne "PUBLISH" dans l'application, et non "publish".

En utilisant une valeur en majuscules au lieu de la valeur en minuscules attendue, la logique dans l'application peut être perturbée. En effet, exécuter le code suivant dans WordPress ne fonctionne pas :

// Ceci récupérera toutes les publications, pas seulement les publiées
$published_posts = get_posts([
  "post_status" => "PUBLISH",
]);

Dans ce cas, nous pouvons envisager d'échanger convention contre commodité, en utilisant toujours un enum pour mapper les constantes, mais en minuscules :

enum Status {
  publish
  draft
  pending
  trash
}

En d'autres termes, nous pouvons trouver un juste milieu entre être rigoureux et être pratique. Nous devrions utiliser les bonnes pratiques lors de la construction du schéma GraphQL, mais nous permettre de nous en écarter chaque fois que cela a du sens.