đšđ»âđ» GraphQL comme (une sorte de) langage de programmation
GraphQL, mĂȘme s'il dispose du langage GraphQL, ne serait normalement pas appelĂ© un langage de programmation, car il y a tellement de choses que nous pouvons faire avec les langages de programmation que nous ne pouvons pas faire avec GraphQL.
GraphQL est normalement utilisé pour récupérer des données, par exemple pour rendre un site web cÎté client, et pour muter des données, par exemple pour créer un article. Et c'est à peu prÚs tout.
(D'autres utilisations sont simplement des combinaisons de ces 2 cas précédents. Par exemple, une passerelle API peut récupérer/muter des données depuis un serveur interne, qui n'est pas exposé au client.)
Accéder aux données en GraphQL :
query PrintPostTitle($postID: ID!)
{
post(by: { id: $postID }) {
title
}
}...a cet équivalent (en quelque sorte) en PHP :
function printPostTitle(int $postID)
{
$post = getPost($postID);
echo $post->title;
}(Tous les exemples ci-dessous utiliseront PHP comme langage de programmation pour la comparaison.)
Muter des données en GraphQL :
query UpdatePost($postID: ID!, $title: String!)
{
updatePost(
by: { id: $postID },
input: { title: $title }
) {
title
}
}...a cet équivalent (en quelque sorte) en PHP :
function updatePost(int $postID, string $title)
{
$post = getPost($postID);
$post->update(['title' => $title]);
}C'est suffisant car GraphQL est normalement accessible depuis un client (codé dans un certain langage de programmation, tel que JavaScript, PHP, Java, ou autre) qui contiendra la logique de ce qu'il faut faire avec les données. Donc GraphQL n'est pas utilisé seul, mais comme compagnon de quelqu'un d'autre.
Mais si GraphQL pouvait ĂȘtre utilisĂ© seul, alors de nombreux nouveaux cas d'usage pourraient ĂȘtre rĂ©solus juste en utilisant GraphQL, permettant Ă GraphQL d'ĂȘtre dĂ©ployĂ© dans de nouveaux environnements et d'ĂȘtre responsable de tĂąches supplĂ©mentaires dans la pile applicative.
Pour que cela se produise, cependant, GraphQL doit supporter de nombreuses fonctionnalités des langages de programmation.
Les fonctionnalitĂ©s de langage de programmation que GraphQL supporte sont limitĂ©es. Par exemple, utiliser la directive @include (ou @skip) et passer une variable en entrĂ©e peut ĂȘtre considĂ©rĂ© comme (une sorte de) logique conditionnelle :
query PrintPostProperties($postID: ID!, $addContent: Boolean!)
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}Cette requĂȘte a cet Ă©quivalent PHP :
function printPostProperties(int $postID, bool $addContent)
{
$post = getPost($postID);
echo $post->title;
if ($addContent) {
echo $post->content;
}
}C'est Ă peu prĂšs tout. GraphQL manque de rĂ©cursions, de variables dynamiques (oĂč leurs valeurs sont calculĂ©es et assignĂ©es Ă la variable lors de l'exĂ©cution, pas comme entrĂ©e dans le dictionnaire), d'assignations de variables (ex : assigner la sortie d'un champ Ă une variable, qui peut ensuite ĂȘtre fournie comme argument Ă un autre champ), et d'autres.
Réfléchissez à la façon dont vous implémenteriez une solution, en utilisant uniquement GraphQL, pour le problÚme suivant :
- CrĂ©er un webhook Ă invoquer par un service chaque fois qu'un nouvel utilisateur s'inscrit Ă ce service ; l'utilisateur peut s'ĂȘtre abonnĂ© Ă la newsletter (indiquĂ© par le champ
marketing_optindans le payload du webhook) ; dans ce cas, le webhook doit enregistrer l'e-mail de l'utilisateur (dans le champemaildu payload du webhook) dans une liste Mailchimp.
Considérez-vous que c'est faisable ? facile ? difficile ? impossible ?
Chez Gato GraphQL, nous voulons résoudre ce problÚme en utilisant uniquement GraphQL. Et bien d'autres problÚmes. C'est pourquoi nous avons réfléchi intensément à la façon de supporter les caractéristiques des langages de programmation.
Explorons quelles fonctionnalités de programmation nous avons supportées sur notre serveur GraphQL. à la fin de cet article, nous verrons comment nous pouvons résoudre ce problÚme.
Fonctionnalité
Les champs en GraphQL apportent normalement des données, comme le titre, le contenu ou les données d'un article. Mais nous pouvons aussi implémenter des champs comme « fonctionnalité ».
Par exemple, imprimer l'heure en PHP :
function printTime()
{
echo time();
}...peut ĂȘtre fait avec le champ _time en GraphQL :
{
_time
}Remarquez que la fonction time n'appartient à aucun type, donc le champ _time non plus. En tant que tel, c'est un champ global, et il est accessible sous chaque type du schéma GraphQL :
{
posts {
_time
}
}D'autres exemples de champs de fonctionnalité sont :
_arrayItem_arrayJoin_date_equals_inArray_intAdd_isEmpty_isNull_makeTime_objectProperty_sprintf_strContains_strRegexReplace_strSubstr
Fonctions
Nous pouvons diviser des unités de logique en fonctions, et avoir une fonction qui en invoque une autre :
function printPostProperties(int $postID)
{
$post = getPost($postID);
printPostTitle();
printPostContent();
}
function printPostTitle(Post $post)
{
echo $post->title;
}
function printPostContent(Post $post)
{
echo $post->content;
}En GraphQL, nous pouvons de mĂȘme diviser l'opĂ©ration query (ou mutation) du document en plusieurs opĂ©rations query, et faire qu'une opĂ©ration « dĂ©pende » d'autres, en exĂ©cutant celles-lĂ en premier :
query PrintPostTitle($postID: ID!)
{
postWithTitle: post(by: { id: $postID }) {
title
}
}
query PrintPostContent($postID: ID!)
{
postWithContent: post(by: { id: $postID }) {
content
}
}
query PrintPostProperties
@depends(on: [
"PrintPostTitle",
"PrintPostContent"
])
{
# ...
}Dans cette requĂȘte, exĂ©cuter la requĂȘte GraphQL en passant ?operationName=PrintPostProperties Ă l'endpoint exĂ©cutera d'abord les requĂȘtes PrintPostTitle et PrintPostContent, et seulement ensuite PrintPostProperties.
Cela est possible via l'ExĂ©cution de multiples requĂȘtes.
Variables dynamiques
Nous pouvons calculer une valeur et l'assigner à une variable lors de l'exécution. Ensuite, en fonction de cette valeur, nous pouvons exécuter conditionnellement une fonctionnalité ou non :
function printPostProperties(int $postID)
{
$post = getPost($postID);
echo $post->title;
$addContent = isUserLoggedIn();
if ($addContent) {
echo $post->content;
}
}En GraphQL, nous pouvons « exporter » une valeur sous une variable dynamique dans une opération, puis lire cette valeur dans une autre opération :
query ExportAddContent
{
addContent: isUserLoggedIn
@export(as: "addContent")
}
query PrintPostProperties($postID: ID!)
@depends(on: "ExportAddContent")
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}Remarquez que la variable $addContent, qui contient une valeur calculée lors de l'exécution, est lue mais non déclarée dans l'opération PrintPostProperties, car c'est une variable dynamique.
Exécuter des fonctions conditionnellement
Une alternative à l'exemple précédent est de regrouper la logique en fonctions, puis d'exécuter conditionnellement une fonction ou non en fonction de la valeur de la variable dynamique :
function printPostProperties(int $postID)
{
$post = getPost($postID);
printPostTitle();
$addContent = isUserLoggedIn();
if ($addContent) {
printPostContent();
}
}
function printPostTitle(Post $post)
{
echo $post->title;
}
function printPostContent(Post $post)
{
echo $post->content;
}En GraphQL, nous pouvons ajouter la directive @include sur l'opération :
query ExportAddContent
{
addContent: isUserLoggedIn
@export(as: "addContent")
}
query PrintPostTitle($postID: ID!)
{
postWithTitle: post(by: { id: $postID }) {
title
}
}
query PrintPostContent($postID: ID!)
@depends(on: "ExportAddContent")
@include(if: $addContent)
{
postWithContent: post(by: { id: $postID }) {
content
}
}
query PrintPostProperties
@depends(on: [
"PrintPostTitle",
"PrintPostContent"
])
{
# ...
}Maintenant, l'opération PrintPostContent ne sera exécutée que si $addContent est true.
Assigner des variables, les réutiliser en entrée
Modifions légÚrement l'exemple précédent, dans lequel la condition "addContent" était liée au fait que l'utilisateur soit connecté ou non.
Dans cet autre exemple, "addContent" est true chaque fois qu'aujourd'hui est un week-end, ce qui implique une certaine logique Ă calculer :
- Obtenir la date d'aujourd'hui
- La formater en nom du jour, en minuscules
- Vérifier que c'est
"saturday"ou"sunday"
En PHP :
function addContent()
{
$today = time();
$dayName = date('l', $today);
$lcDayName = strtolower($dayName);
$isWeekend = in_array(
$lcDayName,
['saturday', 'sunday']
);
return $isWeekend;
}
function printPostProperties(int $postID)
{
$post = getPost($postID);
echo $post->title;
$addContent = addContent();
if ($addContent) {
echo $post->content;
}
}En GraphQL :
query ExportAddContent
{
today: _time
dayName: _date(format: "l", timestamp: $__today)
lcDayName: _strLowerCase(text: $__dayName)
isWeekend: _inArray(
value: $__lcDayName
array: ["saturday", "sunday"],
)
@export(as: "addContent")
}
query PrintPostProperties($postID: ID!)
@depends(on: "ExportAddContent")
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}Dans l'opĂ©ration ExportAddContent, la valeur de chaque champ interrogĂ© est immĂ©diatement disponible pour les champs en dessous, sous la variable dynamique $__fieldName. De cette façon, la sortie d'un champ peut ĂȘtre immĂ©diatement utilisĂ©e comme entrĂ©e d'un autre champ, dĂ©jĂ dans la mĂȘme opĂ©ration.
Cela est possible grĂące Ă Field to Input.
Modifier dynamiquement une valeur
Dans cet exemple en PHP, nous modifions la valeur d'une variable lorsque l'utilisateur connecté est un administrateur, auquel cas un lien pour modifier l'article est ajouté au contenu de l'article :
function isAdminUser()
{
$user = getCurrentUser();
return in_array("administrator", $user->roles);
}
function printPostContent(int $postID)
{
$post = getPost($postID);
$postContent = $post->content;
$isAdminUser = isAdminUser();
if ($isAdminUser) {
$postContent = sprintf(
'%s<p><a href="%s">%s</a></p>',
$postContent,
$post->edit_url,
'(Admin only) Edit post'
)
}
echo $postContent;
}En GraphQL, nous pouvons exécuter conditionnellement une opération ou une autre, produisant des valeurs différentes pour un champ :
query InitializeDynamicVariables
{
isAdminUser: _echo(value: false)
@export(as: "isAdminUser")
}
query ExportConditionalVariables
@depends(on: "InitializeDynamicVariables")
{
me {
roleNames
isAdminUser: _inArray(
value: "administrator",
array: $__roleNames
)
@export(as: "isAdminUser")
}
}
query RetrieveContentForAdminUser($postId: ID!)
@depends(on: "ExportConditionalVariables")
@include(if: $isAdminUser)
{
post(by: { id : $postId }) {
originalContent: content
wpAdminEditURL
content: _sprintf(
string: "%s<p><a href=\"%s\">%s</a></p>",
values: [
$__originalContent,
$__wpAdminEditURL,
"(Admin only) Edit post"
]
)
}
}
query RetrieveContentForNonAdminUser($postId: ID!)
@depends(on: "ExportConditionalVariables")
@skip(if: $isAdminUser)
{
post(by: { id : $postId }) {
content
}
}
query ExecuteAll
@depends(on: [
"RetrieveContentForAdminUser",
"RetrieveContentForNonAdminUser"
])
{
# ...
}En utilisant les directives @include et @skip avec la mĂȘme variable dynamique en entrĂ©e, les opĂ©rations RetrieveContentForAdminUser et RetrieveContentForNonAdminUser sont mutuellement exclusives.
Itérer des tableaux
Disons que nous voulons itérer les éléments d'un tableau, et convertir ces valeurs en majuscules :
function printUserRolesAsUppercase(int $userID)
{
$user = getUser($userID);
foreach ($user->roles as $role) {
echo strtoupper($role);
}
}En GraphQL, nous pouvons avoir la directive @underEachArrayItem qui itÚre sur les éléments du tableau, et fournit chacune de ces valeurs à la directive suivante dans la chaßne, dans ce cas @strUpperCase :
query PrintUserRolesAsUppercase($userID: ID!)
{
user(by: { id: $userID }) {
roles
@underEachArrayItem
@strUpperCase
}
}Cela est possible grĂące aux directives composables.
Opérations CRUD en masse
CRUD signifie Create (créer), Read (lire), Update (mettre à jour) et Delete (supprimer), ce sont les opérations que nous appliquons sur les ressources (articles, utilisateurs, etc).
Lire en masse en PHP ressemble Ă ceci :
function getPostTitles()
{
$posts = getPosts();
foreach ($posts as $post) {
echo $post->title;
}
}Ce cas d'usage est naturellement satisfait par GraphQL :
query GetPostTitles
{
posts {
title
}
}Mettre Ă jour en masse en PHP ressemble Ă ceci :
function updatePostTitlesAsUppercase()
{
$posts = getPosts();
foreach ($posts as $post) {
$post->update(['title' => strtoupper($post->title)]);
}
}Exécuter des mises à jour en masse en GraphQL est normalement supporté en créant une mutation dédiée updatePosts, qui prend les données pour tous les articles.
Je n'aime pas cette approche, car elle double effectivement le nombre de mutations dans le schéma (une pour muter la ressource unique, une pour muter plusieurs ressources), et nous devons maintenir la logique pour les deux :
updatePost+updatePostscreatePost+createPosts- etc
Ă mon avis, une approche plus Ă©lĂ©gante est d'utiliser les mutations imbriquĂ©es, oĂč la mutation Post.update est appliquĂ©e Ă chacune des ressources interrogĂ©es :
mutation UpdatePostTitlesAsUppercase
{
posts {
title
ucTitle: _strUpperCase(text: $__title)
update(
input: { title: $__ucTitle }
) {
status
post {
title
}
}
}
}La mĂȘme approche fonctionne pour supprimer des ressources :
function deletePosts()
{
$posts = getPosts();
foreach ($posts as $post) {
$post->delete();
}
}En GraphQL :
mutation DeletePosts
{
posts {
delete {
status
}
}
}Pour la création, nous ne passons pas les ressources car elles n'existent pas encore ; à la place, nous fournissons un tableau avec les données d'entrée pour toutes les ressources à créer :
function createPosts()
{
$postDataItems = [
[
'title' => 'First title',
'content' => 'First content',
],
[
'title' => 'Second title',
'content' => 'Second content',
],
];
foreach ($postDataItems as $postDataItem) {
$post = new Post($postDataItem['title'], $postDataItem['content']);
$post->save();
}
}Créer des articles en masse en GraphQL en utilisant une seule mutation createPost est un peu délicat, mais c'est néanmoins faisable.
L'idée est d'itérer sur le tableau avec les données d'entrée, d'assigner chacune sous une variable dynamique $input, puis d'exécuter la mutation createPost en passant cette entrée. Enfin, nous obtenons les IDs résultants des articles créés sous la variable dynamique $createdPostIDs, et nous récupérons leurs données :
mutation CreatePosts
@depends(on: "GetPostsAndExportData")
{
createdPostIDs: _echo(value: [
{
title: "First title",
content: "First content"
},
{
title: "Second title",
content: "Second content"
},
])
@underEachArrayItem(
passValueOnwardsAs: "input"
)
@applyField(
name: "createPost"
arguments: {
input: $input
},
setResultInResponse: true
)
@export(as: "createdPostIDs")
}
query RetrieveCreatedPosts
@depends(on: "CreatePosts")
{
createdPosts: posts(
filter: {
ids: $createdPostIDs,
}
) {
title
content
}
}Envoyer une requĂȘte HTTP (et d'autres fonctions)
Envoyer une requĂȘte HTTP Ă un serveur web peut ĂȘtre satisfait via une fonction dĂ©diĂ©e en PHP, comme file_get_contents ou curl_exec.
Avec file_get_contents :
$xml = file_get_contents("http://www.example.com/file.xml");En GraphQL, la logique d'exĂ©cution d'une requĂȘte HTTP peut ĂȘtre satisfaite via un champ de fonctionnalitĂ©, comme _sendHTTPRequest :
query {
_sendHTTPRequest(input: {
url: "http://www.example.com/file.xml",
method: GET
}) {
xml: body
}
}Le mĂȘme concept s'applique Ă n'importe quelle fonctionnalitĂ©.
Par exemple, nous accédons à la valeur d'une constante en PHP comme ceci :
$mailchimpUsername = constant('MAILCHIMP_API_CREDENTIALS_USERNAME');Nous pouvons implémenter un champ de fonctionnalité correspondant en GraphQL :
{
mailchimpUsername: _env(name: "MAILCHIMP_API_CREDENTIALS_USERNAME")
}Résoudre le défi en utilisant uniquement GraphQL
Avec toutes les fonctionnalités de langage de programmation que nous venons de couvrir, nous sommes maintenant capables d'utiliser uniquement GraphQL pour résoudre le problÚme posé plus tÎt :
- CrĂ©er un webhook Ă invoquer par un service chaque fois qu'un nouvel utilisateur s'inscrit Ă ce service ; l'utilisateur peut s'ĂȘtre abonnĂ© Ă la newsletter (indiquĂ© par le champ
marketing_optindans le payload du webhook) ; dans ce cas, le webhook doit enregistrer l'e-mail de l'utilisateur (dans le champemaildu payload du webhook) dans une liste Mailchimp.
La solution est d'utiliser une persisted query GraphQL comme webhook, avec cette requĂȘte :
query HasSubscribedToNewsletter {
hasSubscriberOptIn: _httpRequestHasParam(name: "marketing_optin")
subscriberOptIn: _httpRequestStringParam(name: "marketing_optin")
isNotSubscriberOptInNAValue: _notEquals(value1: $__subscriberOptIn, value2: "NA")
subscribedToNewsletter: _and(values: [$__hasSubscriberOptIn, $__isNotSubscriberOptInNAValue])
@export(as: "subscribedToNewsletter")
}
query MaybeCreateContactOnMailchimp
@depends(on: "HasSubscribedToNewsletter")
@include(if: $subscribedToNewsletter)
{
subscriberEmail: _httpRequestStringParam(name: "email")
mailchimpUsername: _env(name: "MAILCHIMP_API_CREDENTIALS_USERNAME")
mailchimpPassword: _env(name: "MAILCHIMP_API_CREDENTIALS_PASSWORD")
mailchimpListMembersJSONObject: _sendJSONObjectItemHTTPRequest(input: {
url: "https://us7.api.mailchimp.com/3.0/lists/{listCode}/members",
method: POST,
options: {
auth: {
username: $__mailchimpUsername,
password: $__mailchimpPassword
},
json: {
email_address: $__subscriberEmail,
status: "subscribed"
}
}
})
}Dans cette solution, l'opĂ©ration MaybeCreateContactOnMailchimp, qui exĂ©cute la requĂȘte HTTP contre l'API de Mailchimp, sera exĂ©cutĂ©e conditionnellement, en fonction de la valeur du champ marketing_optin.
(Lisez l'article de blog đšđ»âđ« RequĂȘte GraphQL pour envoyer automatiquement les abonnĂ©s Ă la newsletter d'InstaWP Ă Mailchimp pour voir comment cette requĂȘte fonctionne.)
GraphQL est plus puissant que vous ne le pensiez !
GraphQL peut ĂȘtre utilisĂ© pour bien plus que simplement rĂ©cupĂ©rer et muter des donnĂ©es... Adapter les donnĂ©es, modifier dynamiquement la sortie, personnaliser le contenu pour diffĂ©rents contextes, crĂ©er une passerelle API avec Ă peine quelques lignes de code, et bien d'autres choses.
En supportant les fonctionnalités des langages de programmation, nous pouvons résoudre le défi ci-dessus en utilisant uniquement GraphQL, et éviter de déployer un client pour l'accompagner. Nous simplifions ainsi la pile applicative : moins de piÚces mobiles, moins de complexité, moins de code à déboguer, moins de technologies à gérer.
GraphQL, c'est gĂ©nial đ€