đž Comment et oĂč GraphQL peut amĂ©liorer WordPress, en complĂ©ment de la REST API
Mise à jour 01/05/2024 : Découvrez la comparaison Gato GraphQL vs WP REST API.
Le week-end dernier, j'ai publiĂ© l'article de blog đŠžđżââïž Gato GraphQL est maintenant transpilĂ© de PHP 8.0 Ă 7.1.
AprĂšs avoir partagĂ© le post sur Reddit's /r/php, la communautĂ© a lancĂ© une discussion animĂ©e sur l'intĂ©rĂȘt d'utiliser GraphQL dans WordPress, en quoi il diffĂšre de la WP REST API, et dans quelle mesure il est justifiĂ© d'apporter une nouvelle API Ă WordPress.
Je pense que la plupart des commentaires sont pertinents, et que d'autres manquent d'informations clés. GraphQL n'est pas seulement une interface, mais aussi une implémentation. Cela signifie que différents serveurs GraphQL, de différents fournisseurs, peuvent avoir été conçus pour prioriser différentes caractéristiques. En tant que tel, nous ne pouvons pas toujours avoir une attente unifiée de ce que GraphQL offre, ou une compréhension complÚte du fonctionnement d'un moteur GraphQL.
Par exemple, l'expérience GraphQL dans WordPress et dans Laravel sera différente, tout comme l'expérience fournie par les différents serveurs, WPGraphQL ou Gato GraphQL.
Cet article est mon point de vue sur le sujet, répondant à plusieurs des commentaires du post Reddit.
GraphQL vs WP REST API
[C'est une si mauvaise idée] d'avoir une API GraphQL par-dessus WordPress qui utilise déjà sa propre REST API. Utilisez simplement la REST API. [Source]
La REST API et GraphQL servent le mĂȘme objectif : fournir Ă l'application les donnĂ©es dont elle a besoin. Cependant, ils se comportent diffĂ©remment dans la façon dont ils y parviennent : alors que REST a des endpoints prĂ©dĂ©finis fournissant un ensemble spĂ©cifique de donnĂ©es, GraphQL peut fournir exactement les donnĂ©es nĂ©cessaires.
Ce comportement diffĂ©rent peut avoir un impact direct sur les performances de l'application. Avec REST, si nous avons besoin de rĂ©cupĂ©rer une liste d'articles plus des donnĂ©es de chaque auteur de l'article, cela nĂ©cessitera l'envoi de requĂȘtes supplĂ©mentaires. Peut-ĂȘtre 1 requĂȘte supplĂ©mentaire pour toutes les donnĂ©es d'auteur, ou 1 requĂȘte supplĂ©mentaire par auteur. Entre-temps, le visiteur du site web peut attendre que la page soit rendue.
GraphQL amĂ©liore cette situation, car nous pouvons directement rĂ©cupĂ©rer toutes les donnĂ©es d'articles et d'auteurs en une seule requĂȘte, et le rendu de la page web sera plus rapide :
{
posts {
id
title
excerpt
date
url
author {
id
name
url
}
}
}Ainsi, mĂȘme si nous avons dĂ©jĂ la REST API dans WordPress, cela ne signifie pas qu'elle est toujours l'outil le plus adaptĂ© Ă chaque tĂąche. Bien sĂ»r, nous pouvons toujours l'utiliser, mais si nous avons Ă©galement accĂšs Ă GraphQL, alors nous pouvons dĂ©cider d'utiliser cette API chaque fois qu'elle offre un avantage sur REST, et nous en sortirons mieux.
Configuration initiale difficile pour GraphQL + Devoir écrire des resolvers
Il y a certainement un argument selon lequel la configuration initiale pour GraphQL est exponentiellement plus Ă©levĂ©e que pour REST ; vous avez raison que les associations doivent ĂȘtre configurĂ©es. [Source]
Et...
Ce que vous et presque tout le monde sur le web omettez, c'est que pour que ce format d'API fonctionne, vous devez écrire le parser (resolvers + types) qui apporte une série de problÚmes qui ne sont pas présents avec REST. [Source]
Ces commentaires ne sont pas complÚtement exacts, car WPGraphQL et Gato GraphQL ont déjà mappé le modÚle de données WordPress dans le schéma GraphQL (WPGraphQL entiÚrement, mon plugin pour la plupart).
Ensuite, aprÚs avoir installé l'un de ces plugins, vous pouvez immédiatement commencer à récupérer des données pour votre application, sans avoir besoin de créer des resolvers, ou de configurer des associations entre entités.
Il est vrai que, pour rĂ©cupĂ©rer des donnĂ©es personnalisĂ©es des entitĂ©s propres Ă l'application (comme les CPTs), celles-ci doivent ĂȘtre mappĂ©es via des resolvers, et vous devrez le faire. Mais ce n'est pas diffĂ©rent de REST : si vous avez besoin de donnĂ©es personnalisĂ©es de votre CPT, vous devrez crĂ©er un endpoint REST pour rĂ©cupĂ©rer ces donnĂ©es personnalisĂ©es. Un endpoint personnalisĂ© est aussi un resolver.
Ainsi, concernant le besoin de resolvers, REST et l'API GraphQL sont pratiquement identiques.
Maintenant, en parcourant des sites web et de la documentation, cela donne l'impression que GraphQL nécessite plus d'effort de configuration. Il y a donc une part de vérité dans cette présomption.
Je crois qu'il y a quelques raisons Ă cela. PremiĂšrement, GraphQL implique (au moins) deux parties :
- le concept de ce que c'est, et comment ça fonctionne
- les serveurs fournissant une implémentation réelle
En parcourant la documentation de GraphQL, comme le site officiel graphql.org, elle se concentre sur les concepts derriÚre GraphQL, entrant dans les détails des resolvers, ce qu'ils sont et pourquoi ils sont nécessaires.
C'est utile lorsque vous construisez une application de zéro, comme si vous utilisez Laravel et Lighthouse. Dans ce cas, vous avez besoin de coder vos resolvers (mais vous auriez aussi besoin de créer vos endpoints REST).
Cependant, WordPress est dĂ©jĂ l'application, et WPGraphQL et Gato GraphQL sont des solutions. Ces deux plugins ont dĂ©jĂ créé les resolvers pour nous, donc nous n'avons pas Ă nous en inquiĂ©ter (de la mĂȘme façon que la WP REST API fournit Ă©galement un ensemble initial d'endpoints, donc nous n'avons pas Ă nous en inquiĂ©ter).
De plus, GraphQL est plus centrĂ© sur les dĂ©veloppeurs, et sa documentation semble s'adresser directement aux dĂ©veloppeurs. Les dĂ©veloppeurs crĂ©ent les resolvers cĂŽtĂ© serveur, et les dĂ©veloppeurs consomment ces resolvers avec des requĂȘtes personnalisĂ©es cĂŽtĂ© client. Puisque construire des resolvers est une tĂąche pour les dĂ©veloppeurs, cela apparaĂźt naturellement et souvent.
Pour REST, l'attente (je crois) est que l'endpoint fournissant les données requises existera déjà (comme livré par la WP REST API). Si ce n'est pas le cas, seulement alors nous devons nous préoccuper de configurer un endpoint personnalisé. Ainsi, il y a moins d'emphase sur la création de resolvers pour REST.
Ainsi, REST et GraphQL fournissent tous deux les donnĂ©es requises. Mais alors que REST encourage une approche statique, oĂč les endpoints devraient dĂ©jĂ exister, et seulement quand ils n'existent pas nous nous en inquiĂ©tons, GraphQL encourage une approche dynamique, oĂč chaque requĂȘte est faite sur mesure, et nous pouvons alors coder le resolver parfait pour elle.
Donc, en fin de compte, il n'y a pas de différences fondamentales entre REST et GraphQL, juste des interprétations différentes sur la façon dont ils doivent satisfaire leurs exigences.
Vulnérabilités + Considérations de sécurité dans GraphQL
Nous allons voir une énorme vulnérabilité de GraphQL un jour, car écrire des interpréteurs sécurisés est vraiment difficile. [Source]
Et...
WordPress est dĂ©jĂ si massif qu'il a dĂ©jĂ une Ă©norme cible dans le dos ; ajouter N'IMPORTE QUEL plugin ajoute beaucoup de risques, et un plugin offrant d'exposer littĂ©ralement tout WordPress, y compris des nombreux exemples de code pour contourner le modĂšle de sĂ©curitĂ©, c'est un grand non pour moi. La sortie non pilotĂ©e par le thĂšme devrait ĂȘtre aussi restreinte que possible (inexistante Ă moins que je ne le demande) au-delĂ de ce qui est absolument nĂ©cessaire d'exposer. J'espĂšre que cela ne sera jamais intĂ©grĂ© dans le core. [Source]
GraphQL impose en effet des risques de sécurité supplémentaires que nous devons aborder. Je suis entiÚrement d'accord avec ce sentiment.
Mais je ne pense pas que ce soit un problĂšme aussi bloquant, au point d'empĂȘcher une inclusion potentielle de GraphQL dans le core de WP. De plus, je ne pense mĂȘme pas que ce soit vraiment difficile Ă rĂ©soudre.
Ce qui est nĂ©cessaire, c'est que le serveur GraphQL exploite les mĂ©canismes de sĂ©curitĂ© existants de WordPress, puis que le dĂ©veloppeur utilise ces mĂ©canismes, en s'assurant qu'un champ ne peut ĂȘtre accessible que par les utilisateurs appropriĂ©s :
- l'utilisateur est-il connecté ?
- l'utilisateur est-il l'administrateur ?
- l'utilisateur a-t-il un certain rÎle ou une certaine capacité ?
- l'utilisateur est-il l'auteur de l'article ?
Pour satisfaire cette proposition, Gato GraphQL offre des Listes de contrÎle d'accÚs, afin que nous puissions définir qui peut accéder à chaque champ et directive, et par configuration.
Maintenant, parfois utiliser une ACL seule ne suffit pas, et le serveur GraphQL doit fournir des mesures de sécurité supplémentaires. Je vais décrire sur quoi je travaille en ce moment pour la prochaine v0.8 de Gato GraphQL.
Le champ posts (pour récupérer des données d'articles) ne nécessite pas d'autorisation, n'importe quel utilisateur peut y accéder, qu'il soit connecté ou non. Ainsi, pour des raisons de sécurité, il ne récupÚre que les articles publiés.
Mais il y a des situations oĂč nous avons besoin de rĂ©cupĂ©rer aussi des articles en brouillon/en attente/supprimĂ©s, comme :
- Pour construire un site web statique, qui est exécuté par l'administrateur, avec accÚs à toutes les données du site
- Pour les auteurs de l'article, pour lister tous les brouillons afin qu'ils puissent continuer Ă les modifier
J'ai alors élaboré le schéma suivant. Pour récupérer des articles, il y aura 3 champs :
posts: ouvert à tout le monde, peut seulement récupérer des articles publiésmyPosts: ouvert à tout le monde, récupÚre seulement les articles de l'utilisateur connecté, avec n'importe quel statut (publié/brouillon/en attente/supprimé)postsForAdmin: seul l'administrateur peut y accéder, récupÚre n'importe quel article avec n'importe quel statut
Et ensuite, postsForAdmin est dĂ©sactivĂ© par dĂ©faut, donc il n'apparaĂźt mĂȘme pas dans le schĂ©ma GraphQL, Ă moins que l'administrateur ne l'active explicitement (et, trĂšs probablement, il ne sera activĂ© que pour construire des sites statiques).
Une autre situation est lorsqu'un champ peut récupérer à la fois des données publiques et privées. Par exemple, le champ option récupÚre des données de la table wp_options. Certaines entrées sont publiques (comme blogname), tandis que d'autres ne le sont pas (comme admin_email).
Une situation similaire concerne la rĂ©cupĂ©ration des valeurs mĂ©ta, via les champs Post.metaValue, User.metaValue, et autres. Par exemple, les mĂ©tas utilisateur incluent l'entrĂ©e wp_capabilities, qui est certainement privĂ©e, tandis que description est publique. Et puis il y a last_name, qui peut ĂȘtre public ou privĂ© selon l'application.
Pour rendre l'accĂšs Ă ces donnĂ©es sĂ©curisĂ©, le plugin permettra de spĂ©cifier quelles entrĂ©es peuvent ĂȘtre interrogĂ©es via une liste d'autorisation/refus dans la page des paramĂštres, acceptant Ă la fois l'entrĂ©e complĂšte ou une regex :

Ensuite, interroger l'option autorisée fonctionnera, tandis que l'option refusée retournera simplement null :
{
# This option is allowed
siteName: optionValue(name: "blogname")
# This optionValue is not allowed
adminEmail: optionValue(name: "admin_email")
}Avec des mesures de sĂ©curitĂ© appropriĂ©es fournies par le serveur GraphQL, et le bon sens du dĂ©veloppeur, crĂ©er une API GraphQL sĂ©curisĂ©e ne devrait pas ĂȘtre difficile.
GraphQL faisant tomber la BDD
GraphQL est une syntaxe riche permettant d'exprimer des requĂȘtes relationnelles profondes, donc pour un Ă©cosystĂšme comme WordPress, oĂč l'extensibilitĂ© du modĂšle de donnĂ©es provient du pattern entity-attribute-value, cela se traduit par des quantitĂ©s incroyables d'usure sur une base de donnĂ©es, ce qui peut rendre votre site non rĂ©actif si la requĂȘte GraphQL est profonde, compliquĂ©e ou rĂ©cursive. WordPress est dĂ©jĂ cĂ©lĂšbre pour sa capacitĂ© Ă mettre une instance MySQL/MariaDB Ă genoux, donc ajouter GraphQL pourrait rendre les choses bien pires si les requĂȘtes ne sont pas correctement Ă©crites, authentifiĂ©es et limitĂ©es en dĂ©bit. [Source]
Faire tomber la BDD est une préoccupation sérieuse pour les serveurs GraphQL. Je vais décrire comment Gato GraphQL tente d'éviter ce scénario.
Gato GraphQL évite que le problÚme N+1 se produise, déjà par conception architecturale. Il y parvient en rendant le moteur responsable du chargement des entités depuis la base de données, pas le développeur.
Lors de la rĂ©solution de connexions dans un resolver, la valeur retournĂ©e est l'ID (ou liste d'IDs) du/des objet(s), et non l'objet lui-mĂȘme. Par exemple, rĂ©cupĂ©rer l'auteur du custom post se fait ainsi :
class CustomPostFieldResolver extends AbstractDBDataFieldResolver
{
private CustomPostUserTypeAPIInterface $customPostUserTypeAPI;
public function getClassesToAttachTo(): array
{
return [
CustomPostFieldInterfaceResolver::class,
];
}
public function getSchemaFieldType(string $fieldName): ?string
{
return match($fieldName) {
'author' => SchemaDefinition::TYPE_ID,
default => null,
};
}
public function resolveValue(
TypeResolverInterface $typeResolver,
object $customPost,
string $fieldName,
array $fieldArgs = []
): mixed {
switch ($fieldName) {
case 'author':
return $this->customPostUserTypeAPI->getAuthorID($customPost);
}
return null;
}
public function resolveFieldTypeResolverClass(
TypeResolverInterface $typeResolver,
string $fieldName
): ?string {
switch ($fieldName) {
case 'author':
return UserTypeResolver::class;
}
return null;
}
}Disposant de l'ID de l'entité BDD depuis resolveValue, et du type de l'objet depuis resolveFieldTypeResolverClass (représenté via la classe UserTypeResolver), le moteur GraphQL peut alors charger les données de l'objet.
Pour charger les donnĂ©es, le moteur utilise un algorithme super efficace : il a une complexitĂ© temporelle O(n), oĂč n est le nombre de types dans la requĂȘte, pas le nombre de nĆuds.
L'algorithme atteint cette efficacité car il ne parcourt pas un graphe, mais il convertit la structure de données en une pile de composants, qui est beaucoup plus simple à résoudre. (Le « graph » dans GraphQL est un concept, pas une implémentation réelle.)
Ainsi, mĂȘme si la requĂȘte a plusieurs niveaux, chacun rĂ©cupĂ©rant de nombreuses entitĂ©s, l'algorithme peut toujours le gĂ©rer assez bien. Par exemple, il n'y a pas grand impact lors de l'exĂ©cution de la requĂȘte suivante, qui a une profondeur de 10 niveaux :
{
posts(pagination: { limit: 10 }) {
excerpt
title
url
author {
name
url
posts(pagination: { limit: 10 }) {
title
tags(pagination: { limit: 10 }) {
slug
url
posts(pagination: { limit: 10 }) {
title
comments(pagination: { limit: 10 }) {
content
date
author {
name
posts(pagination: { limit: 10 }) {
title
url
comments(pagination: { limit: 10 }) {
content
date
author {
name
username
url
}
}
}
}
}
}
}
}
}
}
}L'exception Ă cette efficacitĂ© est lors de la rĂ©cupĂ©ration de valeurs mĂ©ta, via Post.metaValue, User.metaValue, Comment.metaValue, PostTag.metaValue et PostCategory.metaValue (et aussi leur champ metaValues). C'est parce que les fonctions WordPress (get_post_meta, get_user_meta, etc) rĂ©cupĂšrent des donnĂ©es pour 1 ID Ă la fois, ce qui signifie que chaque entitĂ© nĂ©cessitera un appel Ă la base de donnĂ©es pour rĂ©cupĂ©rer sa valeur mĂ©ta. En consĂ©quence, la rĂ©solution des valeurs mĂ©ta monte en fonction du nombre de nĆuds, pas du nombre de types (le commentaire de l'OP fait mouche, Ă cet Ă©gard).
Pour éviter que des acteurs malveillants utilisent et abusent des champs méta, Gato GraphQL (en v0.8) sera livré avec ces champs désactivés par défaut. Ensuite, l'administrateur doit les activer explicitement et, ce faisant, peut placer ces champs sous une Liste de contrÎle d'accÚs, de sorte qu'à aucun moment la BDD ne soit à risque d'attaque.
Le rate limiting est aussi une excellente idée, je prévois de le supporter pour une prochaine version.
Et puis il y a l'analyse et l'imposition de limitations sur la complexitĂ© de la requĂȘte (comme le nombre de niveaux en profondeur). Le serveur GraphQL rĂ©sout la requĂȘte avec une complexitĂ© temporelle O(n), donc il n'y a pas grand dommage qui puisse ĂȘtre fait concernant les boucles. Cependant, une seule requĂȘte pourrait tout de mĂȘme rĂ©cupĂ©rer des quantitĂ©s illimitĂ©es de donnĂ©es depuis la BDD, et c'est quelque chose que nous pourrions vouloir Ă©viter.
Par exemple, cette simple requĂȘte apportera une quantitĂ© Ă©norme de donnĂ©es en une seule requĂȘte (mon site de dĂ©mo a Ă peine quelques centaines d'enregistrements, donc je peux me permettre de dĂ©montrer l'exĂ©cution de la requĂȘte) :
{
posts000: posts(pagination: { limit: 100 }) {
...PostFields
}
posts100: posts(pagination: { limit: 100, offset: 100 }) {
...PostFields
}
posts200: posts(pagination: { limit: 100, offset: 200 }) {
...PostFields
}
posts300: posts(pagination: { limit: 100, offset: 300 }) {
...PostFields
}
posts400: posts(pagination: { limit: 100, offset: 400 }) {
...PostFields
}
posts500: posts(pagination: { limit: 100, offset: 500 }) {
...PostFields
}
posts600: posts(pagination: { limit: 100, offset: 600 }) {
...PostFields
}
posts700: posts(pagination: { limit: 100, offset: 700 }) {
...PostFields
}
posts800: posts(pagination: { limit: 100, offset: 800 }) {
...PostFields
}
posts900: posts(pagination: { limit: 100, offset: 900 }) {
...PostFields
}
}
fragment PostFields on Post {
id
title
content
date
}Comme on peut le constater, la requĂȘte n'a mĂȘme pas besoin d'ĂȘtre imbriquĂ©e pour crĂ©er des problĂšmes. Donc analyser la complexitĂ© d'une requĂȘte est une affaire dĂ©licate, qui nĂ©cessitera un rĂ©glage fin pour ĂȘtre utile.
J'espĂšre supporter aussi l'analyse des requĂȘtes, mais ce n'est pas dans ma liste de prioritĂ©s Ă©levĂ©es, car avec une combinaison des autres fonctionnalitĂ©s (comme les persisted queries ou les custom endpoints, couplĂ©s avec des Listes de contrĂŽle d'accĂšs) nous pouvons dĂ©jĂ tenir les acteurs malveillants Ă l'Ă©cart, et nous-mĂȘmes ne devrions pas (ne devrions pas !) abuser de notre propre service GraphQL.