👨🏻💻 GraphQL come (una specie di) linguaggio di programmazione
GraphQL, anche se dispone del linguaggio GraphQL, normalmente non verrebbe chiamato un linguaggio di programmazione, perché ci sono così tante cose che possiamo fare con i linguaggi di programmazione che non possiamo fare con GraphQL.
GraphQL è normalmente utilizzato per recuperare dati, ad esempio per renderizzare un sito web sul client, e per mutare dati, ad esempio per creare un articolo. E praticamente è tutto qui.
(Altri usi sono semplicemente combinazioni di questi 2 casi precedenti. Ad esempio, un gateway API può recuperare/mutare dati da un server interno, che non è esposto al client.)
Accedere ai dati in GraphQL:
query PrintPostTitle($postID: ID!)
{
post(by: { id: $postID }) {
title
}
}...ha questo (più o meno) equivalente in PHP:
function printPostTitle(int $postID)
{
$post = getPost($postID);
echo $post->title;
}(Tutti gli esempi qui sotto useranno PHP come linguaggio di programmazione per il confronto.)
Mutare i dati in GraphQL:
query UpdatePost($postID: ID!, $title: String!)
{
updatePost(
by: { id: $postID },
input: { title: $title }
) {
title
}
}...ha questo (più o meno) equivalente in PHP:
function updatePost(int $postID, string $title)
{
$post = getPost($postID);
$post->update(['title' => $title]);
}Questo è sufficiente perché GraphQL è normalmente accessibile da un client (scritto in un qualche linguaggio di programmazione, come JavaScript, PHP, Java o altri) che conterrà la logica di cosa fare con i dati. Quindi GraphQL non viene usato da solo, ma come compagno di qualcun altro.
Ma se GraphQL potesse essere usato da solo, allora molti nuovi casi d'uso potrebbero essere risolti utilizzando soltanto GraphQL, permettendo a GraphQL di essere distribuito in ambienti inediti e di essere responsabile di compiti aggiuntivi nello stack applicativo.
Affinché ciò accada, però, GraphQL deve supportare molte delle funzionalità dei linguaggi di programmazione.
Le funzionalità dei linguaggi di programmazione che GraphQL supporta sono limitate. Ad esempio, usare la direttiva @include (o @skip) e passare una variabile come input può essere considerato (una specie di) logica condizionale:
query PrintPostProperties($postID: ID!, $addContent: Boolean!)
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}Questa query ha questo equivalente PHP:
function printPostProperties(int $postID, bool $addContent)
{
$post = getPost($postID);
echo $post->title;
if ($addContent) {
echo $post->content;
}
}Praticamente è tutto qui. GraphQL manca di ricorsioni, variabili dinamiche (in cui i loro valori sono calcolati e assegnati alla variabile a runtime, non come input nel dizionario), assegnazioni di variabili (es: assegnare l'output di un campo a una variabile, che può poi essere fornita come argomento a un altro campo), e altro ancora.
Considera come implementeresti una soluzione, utilizzando soltanto GraphQL, per il seguente problema:
- Creare un webhook da invocare da parte di un servizio ogni volta che un nuovo utente si registra a quel servizio; l'utente potrebbe essersi iscritto alla newsletter (indicato dal campo
marketing_optinnel payload del webhook); in tal caso, il webhook deve registrare l'email dell'utente (nel campoemaildel payload del webhook) in una lista Mailchimp.
Lo consideri fattibile? facile? difficile? impossibile?
In Gato GraphQL, vogliamo risolvere questo problema utilizzando soltanto GraphQL. E molti altri problemi. Ecco perché abbiamo riflettuto a lungo su come supportare le caratteristiche dei linguaggi di programmazione.
Esploriamo quali funzionalità di programmazione abbiamo supportato sul nostro server GraphQL. Alla fine di questo articolo, vedremo come possiamo risolvere quel problema.
Funzionalità
I campi in GraphQL normalmente portano dati, come il titolo, il contenuto o i dati di un articolo. Ma possiamo anche implementare i campi come "funzionalità".
Ad esempio, stampare l'ora in PHP:
function printTime()
{
echo time();
}...può essere fatto con il campo _time in GraphQL:
{
_time
}Nota che la funzione time non appartiene ad alcun tipo, quindi neanche il campo _time. In quanto tale, è un campo globale, ed è accessibile sotto ogni tipo dello schema GraphQL:
{
posts {
_time
}
}Altri esempi di campi di funzionalità sono:
_arrayItem_arrayJoin_date_equals_inArray_intAdd_isEmpty_isNull_makeTime_objectProperty_sprintf_strContains_strRegexReplace_strSubstr
Funzioni
Possiamo suddividere unità di logica in funzioni, e fare in modo che una funzione ne invochi un'altra:
function printPostProperties(int $postID)
{
$post = getPost($postID);
printPostTitle();
printPostContent();
}
function printPostTitle(Post $post)
{
echo $post->title;
}
function printPostContent(Post $post)
{
echo $post->content;
}In GraphQL, possiamo analogamente suddividere l'operazione query (o mutation) del documento in più operazioni query, e fare in modo che un'operazione "dipenda" da altre, eseguendo quelle per prime:
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"
])
{
# ...
}In questa query, eseguire la query GraphQL passando ?operationName=PrintPostProperties all'endpoint eseguirà prima le query PrintPostTitle e PrintPostContent, e solo dopo PrintPostProperties.
Questo è possibile tramite l'Esecuzione di query multiple.
Variabili dinamiche
Possiamo calcolare un valore e assegnarlo a una variabile a runtime. Poi, in base a quel valore, possiamo eseguire condizionalmente una funzionalità oppure no:
function printPostProperties(int $postID)
{
$post = getPost($postID);
echo $post->title;
$addContent = isUserLoggedIn();
if ($addContent) {
echo $post->content;
}
}In GraphQL, possiamo "esportare" un valore sotto una variabile dinamica in un'operazione, e poi leggere questo valore in un'altra operazione:
query ExportAddContent
{
addContent: isUserLoggedIn
@export(as: "addContent")
}
query PrintPostProperties($postID: ID!)
@depends(on: "ExportAddContent")
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}Nota che la variabile $addContent, che contiene un valore calcolato a runtime, viene letta ma non dichiarata nell'operazione PrintPostProperties, poiché è una variabile dinamica.
Eseguire funzioni condizionalmente
Un'alternativa all'esempio precedente è raggruppare la logica in funzioni, e poi eseguire condizionalmente una funzione oppure no in base al valore della variabile dinamica:
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;
}In GraphQL possiamo aggiungere la direttiva @include sull'operazione:
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"
])
{
# ...
}Ora, l'operazione PrintPostContent verrà eseguita solo se $addContent è true.
Assegnare variabili, riutilizzarle come input
Modifichiamo leggermente l'esempio precedente, in cui la condizione "addContent" era legata al fatto che l'utente fosse connesso oppure no.
In quest'altro esempio, "addContent" è true ogni volta che oggi è weekend, il che comporta una certa logica da calcolare:
- Ottenere la data di oggi
- Formattarla nel nome del giorno, in minuscolo
- Verificare che sia
"saturday"o"sunday"
In 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;
}
}In 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)
}
}Nell'operazione ExportAddContent, il valore di ogni campo interrogato è immediatamente disponibile per i campi sottostanti, sotto la variabile dinamica $__fieldName. In questo modo l'output di un campo può essere immediatamente utilizzato come input di un altro campo, già all'interno della stessa operazione.
Questo è possibile grazie a Field to Input.
Modificare dinamicamente un valore
In questo esempio in PHP, modifichiamo il valore di una variabile quando l'utente connesso è un amministratore, nel qual caso al contenuto dell'articolo viene aggiunto un link per modificare l'articolo:
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;
}In GraphQL, possiamo eseguire condizionalmente un'operazione oppure un'altra, producendo valori diversi per un campo:
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"
])
{
# ...
}Usando le direttive @include e @skip con la stessa variabile dinamica come input, le operazioni RetrieveContentForAdminUser e RetrieveContentForNonAdminUser sono mutuamente esclusive.
Iterare array
Diciamo che vogliamo iterare gli elementi di un array, e convertire quei valori in maiuscolo:
function printUserRolesAsUppercase(int $userID)
{
$user = getUser($userID);
foreach ($user->roles as $role) {
echo strtoupper($role);
}
}In GraphQL, possiamo fare in modo che la direttiva @underEachArrayItem itera sugli elementi dell'array, e fornisca ciascuno di quei valori alla direttiva successiva nella catena, in questo caso @strUpperCase:
query PrintUserRolesAsUppercase($userID: ID!)
{
user(by: { id: $userID }) {
roles
@underEachArrayItem
@strUpperCase
}
}Questo è possibile grazie alle direttive componibili.
Operazioni CRUD in blocco
CRUD sta per Create (creare), Read (leggere), Update (aggiornare) e Delete (eliminare), queste sono le operazioni che applichiamo sulle risorse (articoli, utenti, ecc).
Leggere in blocco in PHP appare così:
function getPostTitles()
{
$posts = getPosts();
foreach ($posts as $post) {
echo $post->title;
}
}Questo caso d'uso è naturalmente soddisfatto da GraphQL:
query GetPostTitles
{
posts {
title
}
}Aggiornare in blocco in PHP appare così:
function updatePostTitlesAsUppercase()
{
$posts = getPosts();
foreach ($posts as $post) {
$post->update(['title' => strtoupper($post->title)]);
}
}Eseguire aggiornamenti in blocco in GraphQL è normalmente supportato creando una mutation dedicata updatePosts, che prende i dati per tutti gli articoli.
Non mi piace questo approccio, perché di fatto raddoppia il numero di mutation nello schema (una per mutare la singola risorsa, una per mutare più risorse), e dobbiamo mantenere la logica per entrambe:
updatePost+updatePostscreatePost+createPosts- ecc
A mio parere, un approccio più elegante è usare le mutation annidate, dove la mutation Post.update viene applicata a ciascuna delle risorse interrogate:
mutation UpdatePostTitlesAsUppercase
{
posts {
title
ucTitle: _strUpperCase(text: $__title)
update(
input: { title: $__ucTitle }
) {
status
post {
title
}
}
}
}Lo stesso approccio funziona per eliminare le risorse:
function deletePosts()
{
$posts = getPosts();
foreach ($posts as $post) {
$post->delete();
}
}In GraphQL:
mutation DeletePosts
{
posts {
delete {
status
}
}
}Per la creazione, non passiamo le risorse perché non esistono ancora; invece, forniamo un array con i dati di input per tutte le risorse da creare:
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();
}
}Creare articoli in blocco in GraphQL utilizzando una singola mutation createPost è un po' complicato, ma è comunque fattibile.
L'idea è di iterare sull'array con i dati di input, assegnare ciascuno sotto una variabile dinamica $input, e poi eseguire la mutation createPost passando quell'input. Infine otteniamo gli ID risultanti dagli articoli creati sotto la variabile dinamica $createdPostIDs, e recuperiamo i loro dati:
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
}
}Inviare una richiesta HTTP (e altre funzioni)
Inviare una richiesta HTTP a un server web può essere soddisfatto tramite una funzione dedicata in PHP, come file_get_contents o curl_exec.
Con file_get_contents:
$xml = file_get_contents("http://www.example.com/file.xml");In GraphQL, la logica per eseguire una richiesta HTTP può essere soddisfatta tramite un campo di funzionalità, come _sendHTTPRequest:
query {
_sendHTTPRequest(input: {
url: "http://www.example.com/file.xml",
method: GET
}) {
xml: body
}
}Lo stesso concetto si applica a qualsiasi funzionalità.
Ad esempio, accediamo al valore di una costante in PHP in questo modo:
$mailchimpUsername = constant('MAILCHIMP_API_CREDENTIALS_USERNAME');Possiamo implementare un campo di funzionalità corrispondente in GraphQL:
{
mailchimpUsername: _env(name: "MAILCHIMP_API_CREDENTIALS_USERNAME")
}Risolvere la sfida utilizzando soltanto GraphQL
Con tutte le funzionalità dei linguaggi di programmazione che abbiamo appena coperto, siamo ora in grado di utilizzare soltanto GraphQL per risolvere il problema posto in precedenza:
- Creare un webhook da invocare da parte di un servizio ogni volta che un nuovo utente si registra a quel servizio; l'utente potrebbe essersi iscritto alla newsletter (indicato dal campo
marketing_optinnel payload del webhook); in tal caso, il webhook deve registrare l'email dell'utente (nel campoemaildel payload del webhook) in una lista Mailchimp.
La soluzione è utilizzare una persisted query GraphQL come webhook, con questa query:
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"
}
}
})
}In questa soluzione, l'operazione MaybeCreateContactOnMailchimp, che esegue la richiesta HTTP verso l'API di Mailchimp, sarà eseguita condizionalmente, in base al valore del campo marketing_optin.
(Leggi l'articolo del blog 👨🏻🏫 Query GraphQL per inviare automaticamente gli iscritti alla newsletter da InstaWP a Mailchimp per vedere come funziona questa query.)
GraphQL è più potente di quanto pensassi!
GraphQL può essere utilizzato per molto più che semplicemente recuperare e mutare dati... Adattare i dati, modificare dinamicamente l'output, personalizzare il contenuto per contesti diversi, creare un gateway API con appena poche righe di codice, e molto altro.
Supportando le funzionalità dei linguaggi di programmazione, possiamo risolvere la sfida qui sopra utilizzando soltanto GraphQL, ed evitare di distribuire un client che lo accompagni. Semplifichiamo così lo stack applicativo: meno parti mobili, meno complessità, meno codice da debuggare, meno tecnologie da gestire.
GraphQL spacca 🤘